{
  "version": 1,
  "updated_at": "2026-10-07T03:25:11.179Z",
  "conversation_id": "1791313310605_3q4u6",
  "source_message_count": 283,
  "source_prefix_hash": "sha256:8dcce4fa1e35c6939a34016f305982a9d98869de1269b231520f755eb7fa8fa0",
  "source_last_message_key": "id:ad871339-4d50-4eb4-ac7c-1c35c54479d6",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Context summary:\n\n## Goal\nApply a set of Catia performance optimizations (selective `notifyListeners`, event-driven palette detection, async writes) to the Flutter `fikatia_merkez` app's Windows-CATIA bridge.\n\n## State\n- **Done:**\n  - Confirmed the correct project path (relative-path reads work; git repo root is the parent of `flutter/fikatia_merkez`; `catia_provider.dart` is **untracked** so no git restore available).\n  - **Edit 4 (async writes) verified applied correctly & keeping:** `performance_monitor.dart` L162–166 `_write` is `async` with `await f.writeAsString(`; `catia_provider.dart` L6943 self-test uses `await File(...).writeAsString(`.\n- **In Progress:**\n  - Reverting the broken **Edit 1** (`_pushPaletteState` selective-guard optimization): it was confirmed **corrupted** (duplicate `deskSig` at analyzer line 3174, `prevSig` at 3175) and also **unsound** (guard signature omits `paletteContext`, `selectionCount`/`selectionTypes`, `paletteTileViews` — all of which change the palette payload, so it could produce a stale palette).\n  - Deleting the three dead Edit-1 regions via PowerShell script (`revert_edit1.ps1`): guard block (~L3159–3179), stray `_deskSigOf` helper (~L3136–3148), stray `_paletteStateSig` field (~L1960–3165).\n- **Blocked:**\n  - Stuck in a **path-typping loop** with the `Mamusta` directory segment (tried `Momausta`, `Mamausta`, `Mesta`, `Mestaustu`, etc.). The absolute-path tools (`Select-String`, `editor`, `read_files` with absolute paths) keep failing even though `pwd`/relative-path (`'.\\lib\\services\\...`) tools succeed. The final command had matched lines but did not print the trimmed output, so the cleanup script (`revert_edit1.ps1`) has **not been confirmed run yet**.\n\n## Highlights\n- **Architecture decision on Edit 1:** reverting rather than fixing — the guard's signature cannot safely short-circuit the palette push, so the optimization is fundamentally flawed here. This is a candidate to rework properly under Priorities 1 & 2 (splitting `CatiaProvider` into multiple small `ChangeNotifier`s) at a later session.\n- **Edit 4 is the only change to preserve** — it's the safe, requested async-write fix affecting both `catia_provider.dart` (L6943) and `performance_monitor.dart` (L162–166).\n- The remaining priorities (event-driven Windows hooks for Priority 3, widget decomposition for Priority 5, combined native call for Priority 6) were **not started** — the session consumed entirely on recovering the corrupted Edit 1.\n\n## Next\n1. **Resolve the path issue:** confirm `Mamusta` is the correct directory segment and get a working absolute path (or consistently use the working relative form `'.\\lib\\services\\catia_provider.dart'`). The `Select-String` run matched `_deskSigOf`/`_paletteStateSig` but the output wasn't captured — re-run to get exact line numbers.\n2. **Run `revert_edit1.ps1`** from the confirmed directory to delete the three dead Edit-1 regions.\n3. **Verify with `flutter analyze`** that `catia_provider.dart` compiles clean (no duplicate definitions, no references to removed `_deskSigOf`/`_paletteStateSig`).\n4. **Confirm Edit 4 is still intact** after cleanup (self-test write + `performance_monitor._write`).\n5. Once the tree is green, optionally proceed to the next prioritized item — but only after cleanup is confirmed; don't stack new edits onto a still-broken state.\n\n## Files\n- **Read:** `lib/services/catia_provider.dart`, `lib/services/performance_monitor.dart` (relative paths from `d:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez`).\n- **Edited/created:** `D:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\revert_edit1.ps1` (cleanup script — guard/helper/field deletion), `D:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\fix_catia.ps1` (earlier cleanup attempt). Both `.ps1` scripts need confirmation that they ran successfully and were not blocked by the path errors."
        }
      ],
      "metadata": {
        "kind": "compaction_summary",
        "displayRole": "system",
        "userRunSpan": 1,
        "summary": "## Goal\nApply a set of Catia performance optimizations (selective `notifyListeners`, event-driven palette detection, async writes) to the Flutter `fikatia_merkez` app's Windows-CATIA bridge.\n\n## State\n- **Done:**\n  - Confirmed the correct project path (relative-path reads work; git repo root is the parent of `flutter/fikatia_merkez`; `catia_provider.dart` is **untracked** so no git restore available).\n  - **Edit 4 (async writes) verified applied correctly & keeping:** `performance_monitor.dart` L162–166 `_write` is `async` with `await f.writeAsString(`; `catia_provider.dart` L6943 self-test uses `await File(...).writeAsString(`.\n- **In Progress:**\n  - Reverting the broken **Edit 1** (`_pushPaletteState` selective-guard optimization): it was confirmed **corrupted** (duplicate `deskSig` at analyzer line 3174, `prevSig` at 3175) and also **unsound** (guard signature omits `paletteContext`, `selectionCount`/`selectionTypes`, `paletteTileViews` — all of which change the palette payload, so it could produce a stale palette).\n  - Deleting the three dead Edit-1 regions via PowerShell script (`revert_edit1.ps1`): guard block (~L3159–3179), stray `_deskSigOf` helper (~L3136–3148), stray `_paletteStateSig` field (~L1960–3165).\n- **Blocked:**\n  - Stuck in a **path-typping loop** with the `Mamusta` directory segment (tried `Momausta`, `Mamausta`, `Mesta`, `Mestaustu`, etc.). The absolute-path tools (`Select-String`, `editor`, `read_files` with absolute paths) keep failing even though `pwd`/relative-path (`'.\\lib\\services\\...`) tools succeed. The final command had matched lines but did not print the trimmed output, so the cleanup script (`revert_edit1.ps1`) has **not been confirmed run yet**.\n\n## Highlights\n- **Architecture decision on Edit 1:** reverting rather than fixing — the guard's signature cannot safely short-circuit the palette push, so the optimization is fundamentally flawed here. This is a candidate to rework properly under Priorities 1 & 2 (splitting `CatiaProvider` into multiple small `ChangeNotifier`s) at a later session.\n- **Edit 4 is the only change to preserve** — it's the safe, requested async-write fix affecting both `catia_provider.dart` (L6943) and `performance_monitor.dart` (L162–166).\n- The remaining priorities (event-driven Windows hooks for Priority 3, widget decomposition for Priority 5, combined native call for Priority 6) were **not started** — the session consumed entirely on recovering the corrupted Edit 1.\n\n## Next\n1. **Resolve the path issue:** confirm `Mamusta` is the correct directory segment and get a working absolute path (or consistently use the working relative form `'.\\lib\\services\\catia_provider.dart'`). The `Select-String` run matched `_deskSigOf`/`_paletteStateSig` but the output wasn't captured — re-run to get exact line numbers.\n2. **Run `revert_edit1.ps1`** from the confirmed directory to delete the three dead Edit-1 regions.\n3. **Verify with `flutter analyze`** that `catia_provider.dart` compiles clean (no duplicate definitions, no references to removed `_deskSigOf`/`_paletteStateSig`).\n4. **Confirm Edit 4 is still intact** after cleanup (self-test write + `performance_monitor._write`).\n5. Once the tree is green, optionally proceed to the next prioritized item — but only after cleanup is confirmed; don't stack new edits onto a still-broken state.\n\n## Files\n- **Read:** `lib/services/catia_provider.dart`, `lib/services/performance_monitor.dart` (relative paths from `d:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez`).\n- **Edited/created:** `D:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\revert_edit1.ps1` (cleanup script — guard/helper/field deletion), `D:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\fix_catia.ps1` (earlier cleanup attempt). Both `.ps1` scripts need confirmation that they ran successfully and were not blocked by the path errors.",
        "details": {
          "readFiles": [
            "./lib/services/catia_provider.dart",
            "./lib/services/performance_monitor.dart",
            ".\\lib\\services\\catia_provider.dart",
            "D:/__sistem__/Masaustu/FikoYT.Yeni.Cag/flutter/fikatia_merkez/lib/services/catia_provider.dart",
            "D:/__sistem__/Mesta/FikoYT.Yeni.Cag/flutter/fikatia_merkez/lib/services/catia_provider.dart",
            "d:\\__sistem__\\Mamausta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\catia_provider.dart",
            "d:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\catia_provider.dart",
            "d:\\__sistem__\\Mamusta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\performance_monitor.dart",
            "d:\\__sistem__\\Mestaustu\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\catia_provider.dart",
            "d:\\__sistem__\\Mestaustu\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\performance_monitor.dart",
            "d:\\__sistem__\\Momausta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\catia_provider.dart"
          ],
          "modifiedFiles": [
            "D:/__sistem__/Masaustu/FikoYT.Yeni.Cag/flutter/fikatia_merkez/fix_catia.ps1",
            "D:\\__sistem__\\Mamausta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\revert_edit1.ps1",
            "D:\\__sistem__\\Mamaustu\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\revert_edit1.ps1",
            "d:\\__sistem__\\Masaustu\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\catia_provider.dart",
            "d:\\__sistem__\\Momausta\\FikoYT.Yeni.Cag\\flutter\\fikatia_merkez\\lib\\services\\catia_provider.dart"
          ]
        },
        "tokensBefore": 894091,
        "generatedAt": 1791343511178
      }
    },
    {
      "id": "msg_QidXLDHs",
      "role": "assistant",
      "content": [
        {
          "type": "thinking",
          "thinking": "Good, now I have authoritative ground truth. The broken Edit 1 still has all its markers:\n- Line 1965: stray field `String _paletteStateSig = '';`\n- Lines 3136-3149: stray helper `_deskSigOf`\n- Lines 3159-3180: broken guard block (with duplicate `deskSig` at 3172/3174, `prevSig` at 3173/3175, and `_paletteStateSig = nowSig;` at 3179)\n\nThe analyzer (earlier, via run_commands) confirmed:\n- error - The name 'deskSig' is already defined - catia_provider.dart:3174:11 - duplicate_definition\n- error - The name 'prevSig' is already defined - catia_provider.dart:3175:11 - duplicate_definition\n\nSo my earlier edit that removed the `final ctx = paletteContext;` guard block actually left the method body but the duplicate lines remain. The current state:\n- Lines 3136-3149: helper (still present)\n- Lines 3152-3180: method with the broken guard (still present)\n- Line 1965: field (still present)\n\nI need to cleanly delete:\n1. Lines 3136-3149 (helper + blank)\n2. Lines 3159-3180 (guard block + blank line after)\n3. Line 1960-1965 (field comment + field)\n\nLet me verify the exact boundaries first by reading the specific ranges.\n"
        },
        {
          "type": "text",
          "text": "Now I have authoritative ground truth. The broken Edit 1 is fully present (lines 1965, 3139, 3172-3179) — my earlier edit did NOT revert it. Let me read the exact boundaries of the three dead regions to delete them precisely:\n\n"
        },
        {
          "type": "tool_use",
          "id": "HchIDJqwIjKvQDPNlbl8reuQwiOm8Ris",
          "name": "read_files",
          "input": {
            "files": [
              {
                "path": "./lib/services/catia_provider.dart",
                "start_line": 1958
              },
              {
                "path": "./lib/services/catia_provider.dart",
                "start_line": 3134
              },
              {
                "path": "./lib/services/catia_provider.dart",
                "start_line": 3180
              }
            ]
          }
        }
      ],
      "ts": 1791343460076,
      "modelInfo": {
        "id": "ornith-1.5:35b",
        "provider": "ollama"
      },
      "metrics": {
        "inputTokens": 158231,
        "outputTokens": 530,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0
      }
    },
    {
      "id": "msg_jEtlburw",
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "HchIDJqwIjKvQDPNlbl8reuQwiOm8Ris",
          "name": "read_files",
          "content": [
            {
              "query": "./lib/services/catia_provider.dart:1958-EOF",
              "result": "1958 |   bool _desktopListBusy = false;\n1959 | \n1960 |   /// 🎯 Palet payload'ının koruma imzası. `_pushPaletteState`'ı seçici\n1961 |   /// yapmak için: `_safeNotify` HER veri değişiminde bu metotu çağırır ama\n1962 |   /// payload'u sadece palet-ilişkili veri (ayar, masaüstü listesi, makro\n1963 |   /// sayısı, hedef durumu, ön-plan pid'i) gerçekten değişince inşa ederiz.\n1964 |   /// İmza aynıysa payload + JSON hash atlanır (karenin en pahalı parçası).\n1965 |   String _paletteStateSig = '';\n1966 |   DateTime _psListNextAt = DateTime.fromMillisecondsSinceEpoch(0);\n1967 | \n1968 |   /// Gruplu görünüm: masaüstü başına CATIA grupları. Arama süzgeci uygulanır;\n1969 |   /// arama varken boş gruplar gizlenir. Grup içinde seçili sıralama geçerli.\n1970 |   ///\n1971 |   /// 📌 ÖNBELLEK (perf): `buildDesktopGroups` her çağrıda YENİ grup listesi,\n1972 |   /// YENİ alt-listeler ve sıralama üretir. Gruplu görünümde bu getter her\n1973 |   /// karede okunduğu için 30 kartta kare başına gereksiz tahsis oluyordu.\n1974 |   /// İmza: kart revizyonu + arama uzunluğu + boş masaüstü gösterimi +\n1975 |   /// masaüstü listesinin kimliği.\n1976 |   List<DesktopGroup> get desktopGroups {\n1977 |     final showEmpty = _settings.showEmptyDesktops && _query.isEmpty;\n1978 |     final cached = _groupsCache;\n1979 |     if (cached != null &&\n1980 |         _groupsCacheRev == _listRev &&\n1981 |         _groupsCacheQueryLen == _query.length &&\n1982 |         _groupsCacheShowEmpty == showEmpty &&\n1983 |         identical(_groupsCacheDesktopList, _desktopList)) {\n1984 |       return cached;\n1985 |     }\n1986 |     final groups = buildDesktopGroups(\n1987 |       desktops: _desktopList,\n1988 |       instances: _queryFiltered(),\n1989 |       showEmpty: showEmpty,\n1990 |       sortWithin: _sortInstances,\n1991 |     );\n1992 |     _groupsCache = groups;\n1993 |     _groupsCacheRev = _listRev;\n1994 |     _groupsCacheQueryLen = _query.length;\n1995 |     _groupsCacheShowEmpty = showEmpty;\n1996 |     _groupsCacheDesktopList = _desktopList;\n1997 |     return groups;\n1998 |   }\n1999 | \n2000 |   /// `desktopGroups` önbelleğinin üretildiği masaüstü listesi (kimlik için).\n2001 |   List<DesktopInfo>? _groupsCacheDesktopList;\n2002 | \n2003 |   bool isDesktopCollapsed(String key) =>\n2004 |       _settings.collapsedDesktops.contains(key);\n2005 | \n2006 |   // ── 🎨 Tema (koyu / açık) ───────────────────────────────────────────────\n2007 |   AppThemeMode get themeMode => _settings.themeMode;\n2008 | \n2009 |   /// Paleti uygular (kök MaterialApp `AppColors.mode`u dinleyip baştan\n2010 |   /// kurulur) ve tray popup'ının cam efektini temaya göre boyar.\n2011 |   void _applyTheme(AppThemeMode m) {\n2012 |     AppColors.apply(m);\n2013 |     // 🌓 Alt pencerelere yayın: her pencere kendi isolate'ında olduğu için\n2014 |     // `AppColors` orada kopyalanmış durumdadır; dosya üzerinden bildirilir\n2015 |     // (bkz. services/theme_sync.dart).\n2016 |     unawaited(ThemeSync.publish(m));\n2017 |     unawaited(WindowTrayService.applyThemeEffect(m));\n2018 |   }\n2019 | \n2020 |   Future<void> setThemeMode(AppThemeMode m) async {\n2021 |     if (_settings.themeMode == m) return;\n2022 |     await updateSessionSettings(_settings.copyWith(themeMode: m));\n2023 |   }\n2024 | \n2025 |   Future<void> toggleThemeMode() => setThemeMode(\n2026 |     _settings.themeMode == AppThemeMode.dark\n2027 |         ? AppThemeMode.light\n2028 |         : AppThemeMode.dark,\n2029 |   );\n2030 | \n2031 |   /// Görünümü değiştirir (ayara yazılır); gruplu moda geçince listeyi tazeler.\n2032 |   Future<void> setListMode(ListMode m) async {\n2033 |     if (_settings.listMode == m) return;\n2034 |     await updateSessionSettings(_settings.copyWith(listMode: m));\n2035 |     if (m == ListMode.byDesktop) {\n2036 |       unawaited(refreshDesktopList(force: true));\n2037 |       unawaited(refreshDesktops());\n2038 |     }\n2039 |   }\n2040 | \n2041 |   Future<void> toggleListMode() => setListMode(_settings.listMode.next);\n2042 | \n2043 |   /// Kart görünümü (☰ kompakt / 📺 ekranı kaplar). Ayar `SessionSettings`\n2044 |   /// içinde tutulur; `updateSessionSettings` diske yazıp `_safeNotify`\n2045 |   /// çağırdığı için liste tazelemesi de aynı turda yapılır.\n2046 |   Future<void> setCardViewMode(CardViewMode m) async {\n2047 |     if (_settings.cardViewMode == m) return;\n2048 |     await updateSessionSettings(_settings.copyWith(cardViewMode: m));\n2049 |   }\n2050 | \n2051 |   /// Şeritteki kart görünümü düğmesi: sırayla kompakt ↔ ekran.\n2052 |   Future<void> toggleCardViewMode() =>\n2053 |       setCardViewMode(_settings.cardViewMode.next);\n2054 | \n2055 |   /// Bir masaüstü grubunu daraltır/açar (ayara yazılır).\n2056 |   Future<void> toggleDesktopCollapsed(String key) {\n2057 |     final cur = [..._settings.collapsedDesktops];\n2058 |     if (!cur.remove(key)) cur.add(key);\n2059 |     return updateSessionSettings(_settings.copyWith(collapsedDesktops: cur));\n2060 |   }\n2061 | \n2062 |   Future<void> setShowEmptyDesktops(bool v) {\n2063 |     if (_settings.showEmptyDesktops == v) return Future.value();\n2064 |     return updateSessionSettings(_settings.copyWith(showEmptyDesktops: v));\n2065 |   }\n2066 | \n2067 |   /// Windows masaüstü listesini tazeler. Birincil kaynak native registry\n2068 |   /// okuması (süreç açmaz, ucuz); boş/başarısızsa ve PowerShell motoru\n2069 |   /// seçiliyse PowerShell modülü (≥5 sn aralık, hata olursa 30 sn geri\n2070 |   /// çekilme). Yalnızca liste GERÇEKTEN değiştiyse dinleyicileri uyarır.\n2071 |   Future<void> refreshDesktopList({bool force = false}) async {\n2072 |     if (_disposed || _desktopListBusy) return;\n2073 |     _desktopListBusy = true;\n2074 |     try {\n2075 |       List<DesktopInfo>? list;\n2076 |       final raw = await NativeBridge.listVirtualDesktops();\n2077 |       if (raw != null && raw.isNotEmpty) {\n2078 |         list = [\n2079 |           for (final m in raw)\n2080 |             DesktopInfo(\n2081 |               index: (m['index'] as num?)?.toInt() ?? -1,\n2082 |               name: (m['name'] as String?) ?? '',\n2083 |               visible: m['current'] == true,\n2084 |               guid: (m['guid'] as String?) ?? '',\n2085 |             ),\n2086 |         ];\n2087 |       } else if (_psEngine &&\n2088 |           (force || DateTime.now().isAfter(_psListNextAt))) {\n2089 |         final ps = await VirtualDesktopService.listDesktops();\n2090 |         _psListNextAt = DateTime.now().add(\n2091 |           ps.isEmpty ? const Duration(seconds: 30) : const Duration(seconds: 5),\n2092 |         );\n2093 |         if (ps.isNotEmpty) {\n2094 |           list = [\n2095 |             for (final d in ps)\n2096 |               DesktopInfo(\n2097 |                 index: d.index,\n2098 |                 // Modül isimsiz masaüstü için de varsayılan adı döndürür.\n2099 |                 name: isDefaultDesktopName(d.name) ? '' : d.name,\n2100 |                 visible: d.visible,\n2101 |               ),\n2102 |           ];\n2103 |         }\n2104 |       }\n2105 |       if (list == null || _disposed) return;\n2106 |       if (!sameDesktopList(_desktopList, list)) {\n2107 |         _desktopList = list;\n2108 |         _safeNotify();\n2109 |       }\n2110 |     } finally {\n2111 |       _desktopListBusy = false;\n2112 |     }\n2113 |   }\n2114 | \n2115 |   /// Test kancası: masaüstü listesini doğrudan kurar.\n2116 |   @visibleForTesting\n2117 |   void debugSetDesktopList(List<DesktopInfo> list) {\n2118 |     _desktopList = list;\n2119 |     notifyListeners();\n2120 |   }\n2121 | \n2122 |   List<HistoryEntry> get filteredHistory {\n2123 |     if (_query.isEmpty) return _history;\n2124 |     final q = _query.toLowerCase();\n2125 |     return _history.where((h) => h.searchText.contains(q)).toList();\n2126 |   }\n2127 | \n2128 |   int get problemCount => _instances.where((i) => i.status.isProblem).length;\n2129 | \n2130 |   // ---- Sıralama (Görünüm ▸ ↕ Sırala / toolbar ↕) ----\n2131 | \n2132 |   /// Aktif listenin sıralaması (ayarlardan gelir; diske yazılır).\n2133 |   InstanceSort get instanceSort => _settings.instanceSort;\n2134 | \n2135 |   /// Sıralamayı değiştirir (ayar dosyasına da yazılır) ve kısa bir bildirim\n2136 |   /// gösterir. Bildirim BİLEREK oturum bandına değil masaüstü toast'ına gider:\n2137 |   /// bant, süren bir oturum geri yükleme mesajını EZERDİ. Toast \"Bildirimler\"\n2138 |   /// ayarına uyar (kapalıysa sessiz geçer).\n2139 |   Future<void> setInstanceSort(InstanceSort s) async {\n2140 |     if (_settings.instanceSort == s) return;\n2141 |     // 📌 Sıralama, `filteredInstances` önbelleğinin girdisidir → düşür.\n2142 |     _bumpList();\n2143 |     await updateSessionSettings(_settings.copyWith(instanceSort: s));\n2144 |     if (_settings.toastEnabled) {\n2145 |       unawaited(\n2146 |         NativeBridge.showToast(\n2147 |           title: '↕ Sıralama: ${s.label}',\n2148 |           subtitle: 'CATIA listesi',\n2149 |           durationMs: _settings.toastDurationMs,\n2150 |         ),\n2151 |       );\n2152 |     }\n2153 |   }\n2154 | \n2155 |   /// Toolbar ↕ çipi: sıradaki sıralamaya geçer (sırayla döner).\n2156 |   Future<void> cycleInstanceSort() =>\n2157 |       setInstanceSort(_settings.instanceSort.next);\n2158 | \n2159 |   /// [list]'i seçili sıralamaya göre sıralar.\n2160 |   void _sortInstances(List<CatiaInstance> list) {\n2161 |     // `List.sort` kararlı DEĞİL: birincil anahtarlar eşitse sıra her seferinde\n2162 |     // değişebilir (kartlar zıplar). Bu yüzden son kırıcı HER ZAMAN id'dir.\n2163 |     int tie(int primary, CatiaInstance a, CatiaInstance b) =>\n2164 |         primary != 0 ? primary : a.id.compareTo(b.id);\n2165 | \n2166 |     switch (_settings.instanceSort) {\n2167 |       case InstanceSort.registered:\n2168 |         list.sort(\n2169 |           (a, b) => tie(_registrationNo(a.id) - _registrationNo(b.id), a, b),\n2170 |         );\n2171 |       case InstanceSort.title:\n2172 |         list.sort((a, b) => tie(_collate(a.title, b.title), a, b));\n2173 |       case InstanceSort.desktop:\n2174 |         // Bilinmeyen masaüstü (\"M?\") en sonda; aynı masaüstünde ada göre.\n2175 |         list.sort((a, b) {\n2176 |           final c = _desktopRank(a).compareTo(_desktopRank(b));\n2177 |           if (c != 0) return c;\n2178 |           return tie(_collate(a.title, b.title), a, b);\n2179 |         });\n2180 |       case InstanceSort.status:\n2181 |         // Sorunlular (hata / bağlantı kesildi / COM zaman aşımı) en üstte,\n2182 |         // sonra çalışanlar, en altta boştakiler.\n2183 |         list.sort(\n2184 |           (a, b) => tie(_statusRank(a.status) - _statusRank(b.status), a, b),\n2185 |         );\n2186 |       case InstanceSort.fileName:\n2187 |         list.sort((a, b) => tie(_collate(a.fileName, b.fileName), a, b));\n2188 |     }\n2189 |     // \"Bağlı değil\" yer tutucu kartlar hangi sıralama olursa olsun listenin\n2190 |     // (gruplu görünümde: masaüstü grubunun) SONUNDA durur; kendi aralarındaki\n2191 |     // sıra yukarıdaki sıralamadan korunur.\n2192 |     if (list.any((i) => i.placeholder)) {\n2193 |       final live = list.where((i) => !i.placeholder).toList();\n2194 |       final ghosts = list.where((i) => i.placeholder).toList();\n2195 |       list\n2196 |         ..clear()\n2197 |         ..addAll(live)\n2198 |         ..addAll(ghosts);\n2199 |     }\n2200 |   }\n2201 | \n2202 |   /// Masaüstü numarası; bilinmiyorsa (\"M?\") sona düşecek büyük değer.\n2203 |   static int _desktopRank(CatiaInstance i) => i.desktopNumber ?? (1 << 20);\n2204 | \n2205 |   /// Durum önceliği: 0 sorunlu, 1 çalışıyor, 2 boşta.\n2206 |   static int _statusRank(CatiaStatus s) {\n2207 |     if (s.isProblem) return 0;\n2208 |     if (s.isBusy || s == CatiaStatus.busy) return 1;\n2209 |     return 2;\n2210 |   }\n2211 | \n2212 |   /// Bağlanma sırası: id'nin sonundaki kayıt numarası (\"CATIA-02\" → 2).\n2213 |   /// Numarasız id'ler sona düşer.\n2214 |   static int _registrationNo(String id) {\n2215 |     final m = RegExp(r'(\\d+)\\s*$').firstMatch(id);\n2216 |     return (m == null ? null : int.tryParse(m.group(1)!)) ?? (1 << 20);\n2217 |   }\n2218 | \n2219 |   /// Ada göre karşılaştırma (büyük/küçük harf duyarsız, boşluklar kırpılır).\n2220 |   static int _collate(String a, String b) =>\n2221 |       a.trim().toLowerCase().compareTo(b.trim().toLowerCase());\n2222 | \n2223 |   int _indexOf(String id) => _instances.indexWhere((i) => i.id == id);\n2224 | \n2225 |   /// Kart üzerindeki 👁 butonu: seç + (gerçek CATIA ise) penceresini öne getir.\n2226 |   /// 🪟 Kart widget'ının çağırdığı eylemler, `focusInstance(...)` gibi\n2227 |   /// GERÇEK provider metotlarıdır. Alt penceredeki sahte provider'da bunlar\n2228 |   /// çalışmaz (COM bağlantısı, `_ghosts`, pid eşlemesi yoktur) → düğmeler\n2229 |   /// \"hiçbir şey olmadı\" gibi görünürdü.\n2230 |   ///\n2231 |   /// 📌 ÇÖZÜM: pencere modunda bu metotlar **kanala yönlendirilir**; işi\n2232 |   ///   ana pencerede `handlePaletteTileAction` yapar. Böylece kart widget'ında\n2233 |   ///   25 ayrı çağrı noktasını tek tek değiştirmeye gerek kalmaz ve listedeki\n2234 |   ///   ile penceredeki davranış **yapısal olarak** aynı kalır.\n2235 |   ///\n2236 |   /// ⚠️ Bu köprü, listede olduğu gibi YALNIZ pencere modunda aktiftir; ana\n2237 |   ///   pencerede hiçbir şey değişmez.\n2238 |   void focusInstance(String id) {\n2239 |     // 🛑 SONSÜZ DÖNGÜ KORUMASI (canlı testte yakalandı): alt pencerede bu\n2240 |     //   metot kanala yönlendirir → ana pencerede `handlePaletteTileAction`\n2241 |     //   çalışır → o da `focusInstance` çağırır → kanal → ... sonsuza dek\n2242 |     //   (logda #5..#16888 yığını).\n2243 |     //\n2244 |     //   Sebep: ana pencere de aynı provider olduğu için \"alt pencere\" ayrımı\n2245 |     //   metot içinde kayboluyordu. Çözüm: kanal hedefi **global** bir\n2246 |     //   \"gerçek pencere\" işaretçisi yerine, yalnız ALT PENCEREDE açılan\n2247 |     //   bağlamda yönlendirme yapılır. Ana pencere daima normal yolu izler.\n2248 |     if (_cardWindowMode && !_handlingRemoteAction) {\n2249 |       unawaited(actionFocus(id));\n2250 |       return;\n2251 |     }\n2252 |     _selectedInstanceId = id;\n2253 |     notifyListeners();\n2254 |     unawaited(bringInstanceToFront(id));\n2255 |   }\n2256 | \n2257 |   /// 🛑 Kanal üzerinden gelen bir eylem YÜRÜTÜLÜYOR (yeniden gönderim yok).\n2258 |   ///\n2259 |   /// Bu bayrak, [handlePaletteTileAction] içinde açılır: ana pencere işi\n2260 |   /// yaparken `focusInstance`/`saveDocument` çağrıları KENDİ GERÇEK yollarını\n2261 |   /// izler, kanala geri dönmez. Bayrak `finally` ile her zaman kapanır.\n2262 |   bool _handlingRemoteAction = false;\n2263 | \n2264 |   final Set<String> _focusing = {};\n2265 | \n2266 |   /// COM'a dokunmadan, çalışan CATIA süreçlerinin taramasından [inst]'in\n2267 |   /// penceresini bulur: önce kayıtlı pid ile; olmazsa (pid güvenilmez) pencere\n2268 |   /// başlığı kartın başlığı/CATIA başlığıyla TEK bir süreçte eşleşirse o.\n2269 |   Future<({int hwnd, int pid})?> _windowFromProcessScan(\n2270 |     CatiaInstance inst,\n2271 |   ) async {\n2272 |     final found = await NativeBridge.listCatiaProcesses(_catiaExeNames);\n2273 |     if (found.isEmpty) return null;\n2274 |     final byPid = found.where((p) => p.pid == inst.pid && p.hwnd != 0);\n2275 |     if (byPid.isNotEmpty) return (hwnd: byPid.first.hwnd, pid: byPid.first.pid);\n2276 |     String n(String s) => s.trim().toLowerCase();\n2277 |     final wanted = {n(inst.title), n(inst.caption)}..remove('');\n2278 |     final byTitle = found\n2279 |         .where((p) => p.hwnd != 0 && wanted.contains(n(p.title)))\n2280 |         .toList();\n2281 |     if (byTitle.length == 1) {\n2282 |       return (hwnd: byTitle.first.hwnd, pid: byTitle.first.pid);\n2283 |     }\n2284 |     return null;\n2285 |   }\n2286 | \n2287 |   /// 👁 CATIA'nın penceresini öne getirir ve etkinleştirir. Pencere başka bir\n2288 |   /// sanal masaüstündeyse önce o masaüstüne geçilir (Windows başka\n2289 |   /// masaüstündeki pencereyi öne getiremez), sonra pencere öne alınır.\n2290 |   /// Küçültülmüşse geri yüklenir. Kesin HWND kullanılır (pid'den tahmin YOK).\n2291 |   Future<void> bringInstanceToFront(String id) async {\n2292 |     final inst = _instances.where((i) => i.id == id).firstOrNull;\n2293 |     if (inst == null || inst.pid <= 0 || !_focusing.add(id)) return;\n2294 |     try {\n2295 |       final cached = _windowCache[id];\n2296 |       // COM bağlantısı yoksa başlık-probe'u yapılamaz → doğrudan süreç\n2297 |       // taramasından (pid / başlık) pencere bulunur.\n2298 |       var w = cached ?? (inst.connected ? await _findRealWindow(inst) : null);\n2299 |       w ??= await _windowFromProcessScan(inst);\n2300 |       if (w == null) {\n2301 |         _setSessionMessage('${inst.title}: $_noWindowMsg', warn: true);\n2302 |         return;\n2303 |       }\n2304 |       // 1) Aynı masaüstündeyse doğrudan öne gelir (hızlı yol).\n2305 |       var ok = await NativeBridge.bringWindowToFront(w.hwnd);\n2306 |       var switched = false;\n2307 |       if (!ok) {\n2308 |         // 2) Başka masaüstünde (ya da öne alınamadı) → pencerenin masaüstüne\n2309 |         // geç, sonra birkaç kez öne almayı dene (geçiş animasyonu sürer).\n2310 |         final r = await VirtualDesktopService.switchToDesktopOfWindow(w.hwnd);\n2311 |         switched = r.ok;\n2312 |         if (!r.ok) {\n2313 |           _dlog(\n2314 |             'ODAK-GECIS-HATA $id hwnd=0x${w.hwnd.toRadixString(16)} '\n2315 |             'err=${r.error}',\n2316 |           );\n2317 |         }\n2318 |         for (var k = 0; k < 4 && !ok && !_disposed; k++) {\n2319 |           await Future<void>.delayed(const Duration(milliseconds: 250));\n2320 |           ok = await NativeBridge.bringWindowToFront(w.hwnd);\n2321 |         }\n2322 |       }\n2323 |       _dlog(\n2324 |         'ODAK $id hwnd=0x${w.hwnd.toRadixString(16)} '\n2325 |         'masaustuGecildi=$switched onde=$ok',\n2326 |       );\n2327 |       if (!ok) {\n2328 |         // Pencere kapanmış/HWND geçersiz olabilir → sonraki tıkta yeniden bul.\n2329 |         _windowCache.remove(id);\n2330 |         _setSessionMessage(\n2331 |           '${inst.title}: pencere öne getirilemedi'\n2332 |           '${switched ? '' : ' (masaüstüne geçilemedi — VirtualDesktop modülü kurulu mu?)'}',\n2333 |           warn: true,\n2334 |         );\n2335 |       }\n2336 |     } finally {\n2337 |       _focusing.remove(id);\n2338 |     }\n2339 |   }\n2340 | \n2341 |   /// ▶️ Hesap başlat: boşta/hata durumundaki instance'ı hesaplamaya alır.\n2342 |   void startJob(String id) {\n2343 |     final i = _indexOf(id);\n2344 |     if (i < 0) return;\n2345 |     final cur = _instances[i];\n2346 |     if (cur.status == CatiaStatus.calculating ||\n2347 |         cur.status == CatiaStatus.exporting) {\n2348 |       return;\n2349 |     }\n2350 |     _instances[i] = cur.copyWith(status: CatiaStatus.calculating, progress: 0);\n2351 |     notifyListeners();\n2352 |   }\n2353 | \n2354 |   /// 📤 Export iste: boşta instance'ı exporta alır.\n2355 |   void exportInstance(String id) {\n2356 |     final i = _indexOf(id);\n2357 |     if (i < 0) return;\n2358 |     final cur = _instances[i];\n2359 |     if (cur.status.isBusy) return;\n2360 |     _instances[i] = cur.copyWith(status: CatiaStatus.exporting, progress: 0);\n2361 |     notifyListeners();\n2362 |   }\n2363 | \n2364 |   /// ⏹ Durdur: süren işi boşta durumuna çeker.\n2365 |   void stopInstance(String id) {\n2366 |     final i = _indexOf(id);\n2367 |     if (i < 0) return;\n2368 |     final cur = _instances[i];\n2369 |     if (!cur.status.isBusy) return;\n2370 |     _instances[i] = cur.copyWith(status: CatiaStatus.idle, clearProgress: true);\n2371 |     notifyListeners();\n2372 |   }\n2373 | \n2374 |   /// 🔄 Yeniden başlat: hatalı/kopuk instance'ı boşa alır.\n2375 |   void restartInstance(String id) {\n2376 |     if (_cardWindowMode && !_handlingRemoteAction) {\n2377 |       unawaited(actionRestart(id));\n2378 |       return;\n2379 |     }\n2380 |     final i = _indexOf(id);\n2381 |     if (i < 0) return;\n2382 |     final cur = _instances[i];\n2383 |     _instances[i] = cur.copyWith(status: CatiaStatus.idle, clearProgress: true);\n2384 |     notifyListeners();\n2385 |   }\n2386 | \n2387 |   /// 🔄 Yeniden Aç: CATIA dışarıdan kapanmış (CatiaStatus.disconnected) bir\n2388 |   /// instance'ı ayarlardaki başlatıcıyla tekrar açar ve kapanmadan ÖNCEKİ\n2389 |   /// döküman listesini (yol+ad) yeni CATIA'da açtırır. Oturum geri yükleme\n2390 |   /// ile aynı alt yapıyı (_launchCatia/_waitForNewInstance/openDocuments/\n2391 |   /// _awaitOpenJob) kullanır, tek fark tek bir instance'a odaklanması.\n2392 |   Future<void> reopenInstance(String id) async {\n2393 |     if (_cardWindowMode && !_handlingRemoteAction) {\n2394 |       unawaited(actionRestart(id));\n2395 |       return;\n2396 |     }\n2397 |     if (_sessionBusy) {\n2398 |       _setSessionMessage(\n2399 |         'Başka bir oturum işlemi sürüyor, bitmesini bekleyin.',\n2400 |         warn: true,\n2401 |       );\n2402 |       return;\n2403 |     }\n2404 |     final idx = _indexOf(id);\n2405 |     if (idx < 0) return;\n2406 |     final dead = _instances[idx];\n2407 |     if (dead.status != CatiaStatus.disconnected) return;\n2408 | \n2409 |     final docs = dead.documents.where((d) => d.path.isNotEmpty).toList();\n2410 |     final activeDoc = dead.documents.where((d) => d.isActive).firstOrNull;\n2411 |     final label = dead.title;\n2412 | \n2413 |     _sessionBusy = true;\n2414 |     unawaited(WindowTrayService.current?.showWindow());\n2415 |     try {\n2416 |       _setSessionMessage('⏳ $label: CATIA yeniden başlatılıyor…');\n2417 |       final before = _liveInstances.map((i) => i.id).toSet();\n2418 |       final err = await _launchCatia();\n2419 |       if (err != null) {\n2420 |         _setSessionMessage('⚠ $label: $err', warn: true);\n2421 |         return;\n2422 |       }\n2423 |       _setSessionMessage('⏳ $label: CATIA açılıyor, macro kaydı bekleniyor…');\n2424 |       final live = await _waitForNewInstance(before);\n2425 |       if (live == null) {\n2426 |         _setSessionMessage(\n2427 |           '⚠ $label: CATIA açıldı ama macro kayıt olmadı (zaman aşımı)',\n2428 |           warn: true,\n2429 |         );\n2430 |         return;\n2431 |       }\n2432 | \n2433 |       // Eski (kopuk) kaydı native'den ve listeden kalıcı olarak temizle —\n2434 |       // artık ayrı bir \"CATIA-0N\" olarak yaşamaya devam eden yeni kayıt var.\n2435 |       unawaited(NativeBridge.removeInstance(id));\n2436 |       _instances.removeWhere((i) => i.id == id);\n2437 |       if (_selectedInstanceId == id) _selectedInstanceId = null;\n2438 |       notifyListeners();\n2439 | \n2440 |       if (dead.title != dead.id) renameInstance(live.id, dead.title);\n2441 | \n2442 |       if (docs.isEmpty) {\n2443 |         _setSessionMessage(\n2444 |           '✅ $label: CATIA yeniden açıldı (kayıtlı döküman yok).',\n2445 |         );\n2446 |         return;\n2447 |       }\n2448 | \n2449 |       _setSessionMessage('⏳ $label: ${docs.length} döküman açtırılıyor…');\n2450 |       final jobId = await NativeBridge.openDocuments(live.id, [\n2451 |         for (final d in docs) '${d.path}${d.name}',\n2452 |       ], activate: activeDoc?.name);\n2453 |       if (jobId <= 0) {\n2454 |         _setSessionMessage(\n2455 |           '⚠ $label: döküman açma başlatılamadı (kod $jobId)',\n2456 |           warn: true,\n2457 |         );\n2458 |         return;\n2459 |       }\n2460 |       final tally = _RestoreTally();\n2461 |       await _awaitOpenJob(jobId, label, tally);\n2462 |       final ok = tally.failed == 0 && tally.errors.isEmpty;\n2463 |       final parts = <String>[\n2464 |         '${tally.opened} döküman açıldı',\n2465 |         if (tally.alreadyOpen > 0) '${tally.alreadyOpen} zaten açıktı',\n2466 |         if (tally.failed > 0) '${tally.failed} döküman açılamadı',\n2467 |       ];\n2468 |       _setSessionMessage(\n2469 |         '${ok ? '✅' : '⚠'} $label: CATIA yeniden açıldı — ${parts.join(' · ')}',\n2470 |         warn: !ok,\n2471 |       );\n2472 |     } catch (e, st) {\n2473 |       debugPrint('reopenInstance EXCEPTION: $e\\n$st');\n2474 |       _setSessionMessage('⚠ $label: beklenmeyen hata: $e', warn: true);\n2475 |     } finally {\n2476 |       _sessionBusy = false;\n2477 |     }\n2478 |   }\n2479 | \n2480 |   /// 🔄 Aç (gri \"kayıtlı-kapalı\" hayalet kartı): hiç çalışan süreci olmayan\n2481 |   /// kayıtlı bir CATIA'yı sıfırdan başlatır ve kaydedildiği andaki döküman\n2482 |   /// listesini yeni CATIA'da açtırır. [reopenInstance] ile aynı alt yapıyı\n2483 |   /// (_launchCatia/_waitForNewInstance/openDocuments/_awaitOpenJob) kullanır;\n2484 |   /// tek fark kaynağın `_instances`'taki \"kopuk\" bir kayıt değil, `_ghosts`\n2485 |   /// listesindeki yer tutucu kart olması. Kehribar (\"açık ama bağlanmadı\")\n2486 |   /// hayaletlerde bu buton YOK — orada zaten çalışan bir süreç var ve macro\n2487 |   /// kaydı olmadan COM ile döküman açtırılamıyor.\n2488 |   Future<void> openSavedGhost(String ghostId) async {\n2489 |     if (_cardWindowMode && !_handlingRemoteAction) {\n2490 |       unawaited(actionRestart(ghostId));\n2491 |       return;\n2492 |     }\n2493 |     if (!ghostId.startsWith(_ghostPrefix)) return;\n2494 |     if (_sessionBusy) {\n2495 |       _setSessionMessage(\n2496 |         'Başka bir oturum işlemi sürüyor, bitmesini bekleyin.',\n2497 |         warn: true,\n2498 |       );\n2499 |       return;\n2500 |     }\n2501 |     final g = _ghosts.where((x) => x.id == ghostId).firstOrNull;\n2502 |     if (g == null) return;\n2503 | \n2504 |     final docs = g.documents.where((d) => d.path.isNotEmpty).toList();\n2505 |     final activeDoc = docs.where((d) => d.isActive).firstOrNull;\n2506 |     final label = g.title;\n2507 | \n2508 |     _sessionBusy = true;\n2509 |     unawaited(WindowTrayService.current?.showWindow());\n2510 |     try {\n2511 |       _setSessionMessage('⏳ $label: CATIA başlatılıyor…');\n2512 |       final before = _liveInstances.map((i) => i.id).toSet();\n2513 |       final err = await _launchCatia();\n2514 |       if (err != null) {\n2515 |         _setSessionMessage('⚠ $label: $err', warn: true);\n2516 |         return;\n2517 |       }\n2518 |       _setSessionMessage('⏳ $label: CATIA açılıyor, macro kaydı bekleniyor…');\n2519 |       final live = await _waitForNewInstance(before);\n2520 |       if (live == null) {\n2521 |         _setSessionMessage(\n2522 |           '⚠ $label: CATIA açıldı ama macro kayıt olmadı (zaman aşımı)',\n2523 |           warn: true,\n2524 |         );\n2525 |         return;\n2526 |       }\n2527 | \n2528 |       if (g.hasCustomTitle) renameInstance(live.id, g.title);\n2529 | \n2530 |       if (docs.isEmpty) {\n2531 |         _setSessionMessage('✅ $label: CATIA açıldı (kayıtlı döküman yok).');\n2532 |         return;\n2533 |       }\n2534 | \n2535 |       _setSessionMessage('⏳ $label: ${docs.length} döküman açtırılıyor…');\n2536 |       final jobId = await NativeBridge.openDocuments(live.id, [\n2537 |         for (final d in docs) '${d.path}${d.name}',\n2538 |       ], activate: activeDoc?.name);\n2539 |       if (jobId <= 0) {\n2540 |         _setSessionMessage(\n2541 |           '⚠ $label: döküman açma başlatılamadı (kod $jobId)',\n2542 |           warn: true,\n2543 |         );\n2544 |         return;\n2545 |       }\n2546 |       final tally = _RestoreTally();\n2547 |       await _awaitOpenJob(jobId, label, tally);\n2548 |       final ok = tally.failed == 0 && tally.errors.isEmpty;\n2549 |       final parts = <String>[\n2550 |         '${tally.opened} döküman açıldı',\n2551 |         if (tally.alreadyOpen > 0) '${tally.alreadyOpen} zaten açıktı',\n2552 |         if (tally.failed > 0) '${tally.failed} döküman açılamadı',\n2553 |       ];\n2554 |       _setSessionMessage(\n2555 |         '${ok ? '✅' : '⚠'} $label: CATIA açıldı — ${parts.join(' · ')}',\n2556 |         warn: !ok,\n2557 |       );\n2558 |     } catch (e, st) {\n2559 |       debugPrint('openSavedGhost EXCEPTION: $e\\n$st');\n2560 |       _setSessionMessage('⚠ $label: beklenmeyen hata: $e', warn: true);\n2561 |     } finally {\n2562 |       _sessionBusy = false;\n2563 |     }\n2564 |   }\n2565 | \n2566 |   /// ❌ Kapat: instance'ı listeden düşürür. Gerçek (pid>0) bir CATIA'ysa\n2567 |   /// native taraftaki kaydı da KALICI olarak siler — aksi halde native\n2568 |   /// g_map'te kalmaya devam eder ve bir sonraki push'ta (yeni bağlanan bir\n2569 |   /// CATIA ya da döküman poller'ı) tekrar listeye düşer.\n2570 |   void closeInstance(String id) {\n2571 |     if (_cardWindowMode && !_handlingRemoteAction) {\n2572 |       unawaited(actionRemove(id));\n2573 |       return;\n2574 |     }\n2575 |     // 👻 Yer tutucu kart: kapatılacak bir CATIA yok, yalnızca listeden gizle.\n2576 |     if (id.startsWith(_ghostPrefix) || id.startsWith(openGhostPrefix)) {\n2577 |       dismissGhost(id);\n2578 |       return;\n2579 |     }\n2580 |     final cur = _instances.where((i) => i.id == id).firstOrNull;\n2581 |     _instances.removeWhere((i) => i.id == id);\n2582 |     if (_selectedInstanceId == id) _selectedInstanceId = null;\n2583 |     _closedDocs.remove(id);\n2584 |     notifyListeners();\n2585 |     if (cur != null && cur.pid > 0) {\n2586 |       unawaited(NativeBridge.removeInstance(id));\n2587 |     }\n2588 |   }\n2589 | \n2590 |   /// 🚪 CATIA'yı kapat: çalışan CATIA'nın ana penceresine kapatma isteği\n2591 |   /// (WM_CLOSE) gönderir — pencerenin ✕ düğmesiyle aynı. CATIA kaydedilmemiş\n2592 |   /// dökümanlar için kendi kaydet/kaydetme diyaloğunu gösterir; süreç ZORLA\n2593 |   /// sonlandırılmaz, dolayısıyla veri kaybı riski CATIA'nın kendi sorusuna\n2594 |   /// bırakılır. COM gerekmez → hem bağlı kartta hem kehribar (açık ama\n2595 |   /// bağlanmamış) kartta çalışır. Kart ❌'sinden farkı: ❌ yalnız listeden\n2596 |   /// kaldırır, CATIA'yı kapatmaz. Kapanış bir sonraki taramada/snapshot'ta\n2597 |   /// yansır (kart düşer / \"bağlı değil\" olur).\n2598 |   Future<void> quitCatia(String id) async {\n2599 |     if (_cardWindowMode && !_handlingRemoteAction) {\n2600 |       unawaited(actionQuit(id));\n2601 |       return;\n2602 |     }\n2603 |     final int pid;\n2604 |     final int hwnd;\n2605 |     final String label;\n2606 |     if (id.startsWith(openGhostPrefix)) {\n2607 |       final p = int.tryParse(id.substring(openGhostPrefix.length));\n2608 |       final proc = _scanned.where((x) => x.pid == p).firstOrNull;\n2609 |       if (proc == null || proc.hwnd == 0) {\n2610 |         _setSessionMessage('CATIA penceresi bulunamadı', warn: true);\n2611 |         return;\n2612 |       }\n2613 |       pid = proc.pid;\n2614 |       hwnd = proc.hwnd;\n2615 |       label =\n2616 |           _ghosts.where((g) => g.id == id).firstOrNull?.title ??\n2617 |           'Açık CATIA (pid ${proc.pid})';\n2618 |     } else {\n2619 |       final inst = _instances.where((i) => i.id == id).firstOrNull;\n2620 |       if (inst == null || inst.pid <= 0) {\n2621 |         // Yer tutucu / mock / süreci olmayan kart: kapatılacak CATIA yok.\n2622 |         _setSessionMessage('Kapatılacak çalışan bir CATIA yok', warn: true);\n2623 |         return;\n2624 |       }\n2625 |       final w = _windowCache[id] ?? await _windowFromProcessScan(inst);\n2626 |       if (_disposed) return;\n2627 |       if (w == null) {\n2628 |         _setSessionMessage('${inst.title}: $_noWindowMsg', warn: true);\n2629 |         return;\n2630 |       }\n2631 |       pid = w.pid;\n2632 |       hwnd = w.hwnd;\n2633 |       label = inst.title;\n2634 |     }\n2635 |     final ok = await NativeBridge.closeCatiaWindow(hwnd, pid);\n2636 |     if (_disposed) return;\n2637 |     _dlog('CATIA-KAPAT $id hwnd=0x${hwnd.toRadixString(16)} pid=$pid ok=$ok');\n2638 |     if (ok) {\n2639 |       _setSessionMessage(\n2640 |         '$label: kapatma isteği gönderildi — kaydedilmemiş '\n2641 |         'döküman varsa CATIA sorabilir (CATIA penceresine bak)',\n2642 |       );\n2643 |     } else {\n2644 |       _windowCache.remove(id);\n2645 |       _setSessionMessage(\n2646 |         '$label: kapatma isteği gönderilemedi (pencere '\n2647 |         'bulunamadı ya da uygulama güncel değil)',\n2648 |         warn: true,\n2649 |       );\n2650 |     }\n2651 |   }\n2652 | \n2653 |   /// </> Bağlı bir CATIA'nın İÇİNDE kod çalıştırır (`SystemService.Evaluate`,\n2654 |   /// 27. oturum). Yalnız bağlantısı KURULMUŞ (yeşil) kartlarda çalışır;\n2655 |   /// kehribar/gri hayalet ya da bağlantısı kopuk kartta başlatılmaz.\n2656 |   ///\n2657 |   /// GÜVENLİK (B7): [text] YALNIZ kullanıcının o an elle yazdığı/seçtiği\n2658 |   /// kaynaktan gelmelidir — `.fikoses`, sürükle-bırak ya da dış girdiden\n2659 |   /// ASLA otomatik çağrılmaz. Kod CATIA süresince donduruyor; iptal API'si\n2660 |   /// yok. [timeout] dolarsa `stillRunning=true` döner (script CATIA'da\n2661 |   /// hâlâ sürüyor olabilir). [onTick] her yoklamada geçen ms ile çağrılır.\n2662 |   Future<ScriptRunResult> runScriptOn(\n2663 |     String id,\n2664 |     String text, {\n2665 |     ScriptLanguage language = ScriptLanguage.vbScript,\n2666 |     bool passApp = false,\n2667 |     String func = kScriptDefaultFunc,\n2668 |     Duration timeout = const Duration(minutes: 10),\n2669 |     void Function(int elapsedMs)? onTick,\n2670 |   }) async {\n2671 |     if (!_isRealApp) {\n2672 |       return const ScriptRunResult.failure(\n2673 |         'Kod çalıştırma yalnız gerçek uygulamada (Windows) kullanılabilir',\n2674 |       );\n2675 |     }\n2676 |     if (id.startsWith(_ghostPrefix) || id.startsWith(openGhostPrefix)) {\n2677 |       return const ScriptRunResult.failure(\n2678 |         'Bu kart CATIA\\'ya bağlı değil — önce bağla (🔌)',\n2679 |       );\n2680 |     }\n2681 |     final inst = _instances.where((i) => i.id == id).firstOrNull;\n2682 |     if (inst == null || inst.pid <= 0 || !inst.connected) {\n2683 |       return const ScriptRunResult.failure(\n2684 |         'Bu CATIA\\'ya bağlantı yok (kart yeşil/bağlı olmalı)',\n2685 |       );\n2686 |     }\n2687 |     if (!_beginScript(id)) {\n2688 |       return const ScriptRunResult.failure(\n2689 |         'Bu CATIA\\'da zaten bir kod/makro çalışıyor — bitmesini bekle',\n2690 |       );\n2691 |     }\n2692 |     try {\n2693 |       final jobId = await NativeBridge.runScript(\n2694 |         id,\n2695 |         text,\n2696 |         func: func.trim().isEmpty ? kScriptDefaultFunc : func.trim(),\n2697 |         language: language.code,\n2698 |         passApp: passApp,\n2699 |       );\n2700 |       if (jobId <= 0) {\n2701 |         return ScriptRunResult.failure(scriptStartErrorText(jobId));\n2702 |       }\n2703 |       _dlog(\n2704 |         'KOD-CALISTIR $id job=$jobId lang=${language.code} '\n2705 |         'passApp=$passApp len=${text.length}',\n2706 |       );\n2707 |       return await _awaitScriptJob(id, jobId, timeout, onTick);\n2708 |     } finally {\n2709 |       _endScript(id);\n2710 |     }\n2711 |   }\n2712 | \n2713 |   /// 🧰 Bağlı bir CATIA'da `Application.StartCommand(name)` ile bir CATIA\n2714 |   /// komutunu ikonuna tıklanmış gibi başlatır (31. oturum, F1/F2). Yalnız\n2715 |   /// bağlantısı KURULMUŞ (yeşil) kartlarda; hayalet/kopuk kartta çalışmaz.\n2716 |   /// Kullanıcı girdisi gerektirmeyen komutlar işi bitene kadar bekletir;\n2717 |   /// diyalog/seçim açan komutlarda diyaloğu açıp hemen döner (CATIA'da\n2718 |   /// diyalog açık kalabilir — [timeout] dolarsa `stillRunning=true`).\n2719 |   Future<ScriptRunResult> startCommandOn(\n2720 |     String id,\n2721 |     String name, {\n2722 |     Duration timeout = const Duration(minutes: 10),\n2723 |     void Function(int elapsedMs)? onTick,\n2724 |   }) async {\n2725 |     if (!_isRealApp) {\n2726 |       return const ScriptRunResult.failure(\n2727 |         'Komut çalıştırma yalnız gerçek uygulamada (Windows) kullanılabilir',\n2728 |       );\n2729 |     }\n2730 |     if (id.startsWith(_ghostPrefix) || id.startsWith(openGhostPrefix)) {\n2731 |       return const ScriptRunResult.failure(\n2732 |         'Bu kart CATIA\\'ya bağlı değil — önce bağla (🔌)',\n2733 |       );\n2734 |     }\n2735 |     final inst = _instances.where((i) => i.id == id).firstOrNull;\n2736 |     if (inst == null || inst.pid <= 0 || !inst.connected) {\n2737 |       return const ScriptRunResult.failure(\n2738 |         'Bu CATIA\\'ya bağlantı yok (kart yeşil/bağlı olmalı)',\n2739 |       );\n2740 |     }\n2741 |     if (!_beginScript(id)) {\n2742 |       return const ScriptRunResult.failure(\n2743 |         'Bu CATIA\\'da zaten bir kod/makro çalışıyor — bitmesini bekle',\n2744 |       );\n2745 |     }\n2746 |     try {\n2747 |       final jobId = await NativeBridge.startCommand(id, name);\n2748 |       if (jobId <= 0) {\n2749 |         return ScriptRunResult.failure(scriptStartErrorText(jobId));\n2750 |       }\n2751 |       _dlog('KOMUT-CALISTIR $id job=$jobId \"$name\"');\n2752 |       return await _awaitScriptJob(id, jobId, timeout, onTick);\n2753 |     } finally {\n2754 |       _endScript(id);\n2755 |     }\n2756 |   }\n2757 | \n2758 |   // ---- 🧰 Komut paleti (D9 (4)) ----\n2759 |   //\n2760 |   // Palet, hedef CATIA'nın (I1: YAPIŞKAN hedef — en son ön planda görülen BAĞLI\n2761 |   // CATIA) workbench'ine ve aktif döküman türüne göre hangi kayıtlı öğeleri\n2762 |   // göstereceğine karar verir (O4: öğe başına chip'ler, bkz. palette_model.dart).\n2763 |   // Pencere/stil/konum işi `PaletteService`'te; karar YALNIZ burada (tek durum\n2764 |   // kaynağı) alınır.\n2765 | \n2766 |   PaletteSettings _palette = const PaletteSettings();\n2767 |   PaletteSettings get paletteSettings => _palette;\n2768 | \n2769 |   /// I9: Pin başarılı mı? (`false` = yedek yolda: masaüstü takibi devrede.)\n2770 |   bool _palettePinOk = false;\n2771 | \n2772 |   /// 🎯 Palet hedefi elle mi seçildi? (başlıktaki hedef adına tıklayıp döngü)\n2773 |   /// true iken ön plan takibi hedefi EZMEZ. Oturumluk — palette.json'a\n2774 |   /// YAZILMAZ; uygulama yeniden açılınca otomatikten başlar.\n2775 |   bool _paletteTargetPinned = false;\n2776 |   bool get paletteTargetPinned => _paletteTargetPinned;\n2777 | \n2778 |   /// Paletin şu an olduğu sanal masaüstü GUID'i (yalnız Pin başarısızsa\n2779 |   /// takip için kullanılır; null = henüz bilinmiyor).\n2780 |   String? _paletteDesktopGuid;\n2781 | \n2782 |   /// Yapışkan hedefin kart id'si (null = henüz hedef yok).\n2783 |   String? _paletteTargetId;\n2784 |   String? get paletteTargetId => _paletteTargetId;\n2785 | \n2786 |   /// [_paletteTargetId]'yi günceller VE native'e bildirir (bkz.\n2787 |   /// com_bridge.cpp SetPaletteTargetId): DocPoller sonraki turlardan\n2788 |   /// itibaren yalnız BU id'deki CATIA için workbench/seçim okur. Değer\n2789 |   /// gerçekten değişmediyse gereksiz köprü çağrısı yapmaz.\n2790 |   void _setPaletteTargetId(String? id) {\n2791 |     if (_paletteTargetId == id) return;\n2792 |     _paletteTargetId = id;\n2793 |     unawaited(NativeBridge.setPaletteTargetId(id ?? ''));\n2794 |   }\n2795 | \n2796 |   /// Hedef CATIA kartı — kopmuş/kapanmışsa null (palet düğmeleri soluk çizilir).\n2797 |   CatiaInstance? get paletteTarget {\n2798 |     final id = _paletteTargetId;\n2799 |     if (id == null) return null;\n2800 |     final inst = _instances.where((i) => i.id == id).firstOrNull;\n2801 |     if (inst == null || !inst.connected || inst.pid <= 0) return null;\n2802 |     return inst;\n2803 |   }\n2804 | \n2805 |   /// Palet bağlamı (workbench + aktif döküman türü) — hedef yoksa boş.\n2806 |   PaletteContext get paletteContext {\n2807 |     final inst = paletteTarget;\n2808 |     if (inst == null) return PaletteContext.empty;\n2809 |     final active = inst.documents.where((d) => d.isActive).firstOrNull;\n2810 |     return PaletteContext.fromFields(\n2811 |       workbenchId: inst.workbenchId,\n2812 |       activeDocumentName: active?.name,\n2813 |       selectionCount: inst.selectionCount,\n2814 |       selectionTypes: inst.selectionTypes,\n2815 |     );\n2816 |   }\n2817 | \n2818 |   /// Paletin göstereceği öğeler (kayıt sırası korunur).\n2819 |   List<SavedMacro> get paletteEntries =>\n2820 |       paletteVisibleEntries(_macros, paletteContext);\n2821 | \n2822 |   /// Chip seçenekleri: bilinen workbench kimlikleri (F3) + canlıdan\n2823 |   /// öğrenilenler ([PaletteSettings.seenWorkbenches]).\n2824 |   List<String> get paletteWorkbenchChoices =>\n2825 |       paletteWorkbenchOptions(_palette.seenWorkbenches);\n2826 | \n2827 |   /// Palet görünür mü (etkin + gizlenmemiş)? Popup 🧰 düğmesi bunu izler.\n2828 |   bool get paletteVisible => _palette.enabled && !_palette.hidden;\n2829 | \n2830 |   /// Paleti gösterir (ilk kez: pencereyi oluşturur — I4, kapalıyken engine yok).\n2831 |   Future<void> showPalette() =>\n2832 |       updatePaletteSettings(_palette.copyWith(enabled: true, hidden: false));\n2833 | \n2834 |   Future<void> hidePalette() =>\n2835 |       updatePaletteSettings(_palette.copyWith(enabled: true, hidden: true));\n2836 | \n2837 |   /// 🧰 düğmesi / tray menüsü: aç-kapa.\n2838 |   Future<void> togglePalette() =>\n2839 |       paletteVisible ? hidePalette() : showPalette();\n2840 | \n2841 |   /// Palet ayarını uygular: disk yazımı + pencere/konum + içerik tek elden\n2842 |   /// (pencere servisi tek yazıcı).\n2843 |   Future<void> updatePaletteSettings(PaletteSettings s) async {\n2844 |     _palette = s;\n2845 |     // Tray menüsünün 🧰 modu etiketi de tazelensin (syncMenu ayrıca çağrılır).\n2846 |     WindowTrayService.paletteWhenCatia =\n2847 |         s.showMode == PaletteShowMode.whenCatia;\n2848 |     _safeNotify();\n2849 |     await PaletteService.applySettings(s);\n2850 |     _pushPaletteState();\n2851 |   }\n2852 | \n2853 |   /// Palet düğmesine tıklandı (alt pencereden `run`): öğe, o anki CATIA'da\n2854 |   /// çalıştırılır (O7: yalnız hedef; \"hepsine gönder\" yok).\n2855 |   ///\n2856 |   /// 🧰 HEDEF SEÇİMİ: tıklama anında ön plan TAZE okunur — palet odağı\n2857 |   /// ÇALMADIĞI için ön plandaki pencere, kullanıcının o an çalıştığı CATIA'dır\n2858 |   /// ve öğe ORAYA gitmeli. Eski davranış (yalnız pid DEĞİŞİNCE güncellenen\n2859 |   /// yapışkan id) CATIA kapanıp yeniden kaydolunca bayat kalıp YANLIŞ CATIA'ya\n2860 |   /// gidebiliyordu. Yapışkan hedef yalnızca ön plan BİZİM penceremizken (popup\n2861 |   /// öndeyken tıklama) ya da ön plandaki CATIA kayıtlı değilken devreye girer.\n2862 |   Future<void> runPaletteEntry(String entryId) async {\n2863 |     // 🎯 Elle seçim (pin) varsa taze çözüm YOK — hedef neyse ORAYA gider.\n2864 |     final fresh = _paletteTargetPinned\n2865 |         ? null\n2866 |         : await _resolvePaletteTargetFresh();\n2867 |     final inst = fresh ?? paletteTarget;\n2868 |     if (inst == null) {\n2869 |       _setSessionMessage('🧰 Palet: bağlı CATIA yok', warn: true);\n2870 |       return;\n2871 |     }\n2872 |     debugPrint(\n2873 |       'PALET-RUN: $entryId -> ${inst.id} (pid=${inst.pid}, '\n2874 |       'taze=${fresh != null}, pin=$_paletteTargetPinned)',\n2875 |     );\n2876 |     final m = _macros.where((e) => e.id == entryId).firstOrNull;\n2877 |     if (m == null) return;\n2878 |     if (m.isCommand) {\n2879 |       unawaited(\n2880 |         startCommandOn(inst.id, m.command!).then((r) {\n2881 |           // Hangi CATIA'ya gittiği ilerleme bandında görünsün.\n2882 |           _setSessionMessage(\n2883 |             r.ok\n2884 |                 ? '✔ ${m.name} (${inst.title}): komut gönderildi'\n2885 |                 : '✖ ${m.name} (${inst.title}): ${scriptResultSummary(r)}',\n2886 |             warn: !r.ok,\n2887 |           );\n2888 |         }),\n2889 |       );\n2890 |       return;\n2891 |     }\n2892 |     // announce: bantta \"⏳ … (CATIA-0x) içinde çalışıyor…\" + sonuç görünsün →\n2893 |     // yanlış CATIA hemen fark edilir.\n2894 |     unawaited(runMacroOn(inst.id, m, announce: true));\n2895 |   }\n2896 | \n2897 |   /// 🧰 Palet modu (O2/D2): her zaman ↔ yalnız CATIA/biz öndeyken. Palet\n2898 |   /// başlığındaki 👁/🌐 düğmesinden ya da tray menüsünden gelir; kalıcıdır\n2899 |   /// (palette.json'a yazılır) ve tray menüsünün etiketi de tazelenir.\n2900 |   Future<void> togglePaletteShowMode() async {\n2901 |     final next = _palette.showMode == PaletteShowMode.always\n2902 |         ? PaletteShowMode.whenCatia\n2903 |         : PaletteShowMode.always;\n2904 |     await updatePaletteSettings(_palette.copyWith(showMode: next));\n2905 |     await WindowTrayService.syncMenu();\n2906 |     _setSessionMessage(\n2907 |       next == PaletteShowMode.whenCatia\n2908 |           ? '🧰 Palet: yalnız CATIA/biz ön plandayken görünür'\n2909 |           : '🧰 Palet: her zaman görünür',\n2910 |     );\n2911 |   }\n2912 | \n2913 |   /// showMode == whenCatia iken palet ŞU AN görünür olmalı mı? Ön planda\n2914 |   /// CATIA ya da bizim pencerelerden biri (popup) var mı? [shell] =\n2915 |   /// `getShellState` anlık görüntüsü (PaletteService'in yerleşim turundan).\n2916 |   bool _paletteWantsAutoVisible(PaletteShellState shell) =>\n2917 |       shell.foregroundIsSelf ||\n2918 |       _instances.any(\n2919 |         (i) =>\n2920 |             i.connected &&\n2921 |             _effectivePid(i) > 0 &&\n2922 |             _effectivePid(i) == shell.fgPid,\n2923 |       );\n2924 | \n2925 |   /// Bir kartın ön-plan eşleştirmesinde kullanılacak pid'i: önce native\n2926 |   /// probe'un bulduğu GERÇEK pencere pid'i ([realWindowPid]), henüz yoksa\n2927 |   /// makronun bildirdiği `instance.pid`. Kart vurgusu (catia_list_tile) ve\n2928 |   /// palet hedef çözümü AYNI kuralı kullanır → \"renk değişen kart\" ile\n2929 |   /// \"makronun gittiği CATIA\" her zaman aynı olur.\n2930 |   int _effectivePid(CatiaInstance i) => _windowCache[i.id]?.pid ?? i.pid;\n2931 | \n2932 |   /// I9: palet penceresi yeni kaydedildi → BİR KEZ tüm masaüstlerine sabitle\n2933 |   /// (PowerShell `Pin-Window`). Başarısızsa (R3: modül yok/eski sürüm) yedek\n2934 |   /// devreye girer: paletin o anki masaüstü öğrenilir ve hedef CATIA başka\n2935 |   /// masaüstüne geçtiğinde palet de oraya taşınır ([_maybeFollowTargetDesktop]).\n2936 |   Future<void> _pinPaletteWindow(int hwnd) async {\n2937 |     _palettePinOk = false;\n2938 |     final r = await VirtualDesktopService.pinWindow(hwnd);\n2939 |     _palettePinOk = r.ok;\n2940 |     debugPrint('PALET-PIN: hwnd=$hwnd ok=${r.ok} ${r.error ?? ''}');\n2941 |     if (!r.ok) {\n2942 |       final info = await NativeBridge.getWindowDesktopInfo(hwnd);\n2943 |       _paletteDesktopGuid = (info != null && info.valid) ? info.guid : null;\n2944 |     }\n2945 |   }\n2946 | \n2947 |   /// Pin başarısız olduysa palet hedef CATIA'nın masaüstünü takip eder:\n2948 |   /// hedefin masaüstü değiştiğinde palet penceresi de oraya taşınır\n2949 |   /// (IVirtualDesktopManager kendi penceremizi taşımaya izin verir).\n2950 |   /// Pin OK ise gereksiz — palet zaten her masaüstünde görünür.\n2951 |   Future<void> _maybeFollowTargetDesktop() async {\n2952 |     if (_palettePinOk || !_palette.enabled || _disposed) return;\n2953 |     final hwnd = PaletteService.hwnd;\n2954 |     if (hwnd == null) return;\n2955 |     final t = paletteTarget;\n2956 |     if (t == null || t.desktopGuid.isEmpty) return;\n2957 |     if (t.desktopGuid == _paletteDesktopGuid) return;\n2958 |     final previous = _paletteDesktopGuid;\n2959 |     _paletteDesktopGuid = t.desktopGuid;\n2960 |     final hr = await NativeBridge.moveWindowToDesktop(\n2961 |       _selfPid,\n2962 |       t.desktopGuid,\n2963 |       hwnd: hwnd,\n2964 |     );\n2965 |     debugPrint(\n2966 |       'PALET-TAKIP: palet -> ${t.desktopGuid} hr=$hr '\n2967 |       '(önceki=$previous)',\n2968 |     );\n2969 |   }\n2970 | \n2971 |   /// 🧰 AYNI PID'ye kayıtlı BİRDEN FAZLA kart olabilir (tek CATIA sürecinde\n2972 |   /// çoklu döküman penceresi — kullanıcı loglarında doğrulandı: 19 kart,\n2973 |   /// hepsi pid=9412). Bu durumda pid TEK BAŞINA ayırt edemez; CATIA'nın\n2974 |   /// pencere başlığı ([fgTitle]) aktif dökümanın adını içerdiği için ikinci\n2975 |   /// sinyal olarak kullanılır: `fgTitle` içinde kartın aktif döküman adı\n2976 |   /// GEÇİYORSA o kart seçilir. Hiçbiri eşleşmezse (başlık boş/tanınamadı)\n2977 |   /// önceki hedef adaylar arasındaysa ONA sadık kalınır (rastgele sıçramasın);\n2978 |   /// o da yoksa ilk aday döner (eski davranış — tek adaylık pid'lerde zaten\n2979 |   /// tek sonuç verir).\n2980 |   CatiaInstance? _resolveInstanceForWindow(\n2981 |     int pid,\n2982 |     String fgTitle,\n2983 |     String? preferId,\n2984 |   ) {\n2985 |     // KÖK NEDEN DÜZELTMESİ: `i.pid` (makronun RegisterInstance'ta bildirdiği\n2986 |     // pid) launcher/bat ile başlatılan CATIA'larda GERÇEK pencere pid'inden\n2987 |     // farklı olabilir. Kart vurgusu zaten gerçek pid'e bakıyordu (doğru\n\n[Showing lines 1958-2987 of 10452. Use start_line/end_line to read other sections.]",
              "success": true
            },
            {
              "query": "./lib/services/catia_provider.dart:3134-EOF",
              "result": "3134 |   }\n3135 | \n3136 |   /// Palet-ilişkili masaüstü listesinden ucuz koruma imzası üretir. Yalnızca\n3137 |   /// payload'u etkileyen alanları (`index` + `guid`) tarar — ad/görünürlük\n3138 |   /// değişse bile listedeki CATIA konumu değişmediyse imza aynı kalır.\n3139 |   static String _deskSigOf(List<DesktopInfo> list) {\n3140 |     final b = StringBuffer();\n3141 |     for (final d in list) {\n3142 |       b.write(d.index);\n3143 |       b.write('|');\n3144 |       b.write(d.guid);\n3145 |       b.write(';');\n3146 |     }\n3147 |     return b.toString();\n3148 |   }\n3149 | \n3150 |   /// Palet penceresine içeriği gönderir (etkin değilse hiç iş yapmaz; pencere\n3151 |   /// servisi aynı imzayı tekrar göndermez).\n3152 |   void _pushPaletteState() {\n3153 |     // 🔴 PENCERE MODU: alt pencerede palet YOKTUR (veri ana pencereden\n3154 |     //   gelir). `_safeNotify` her veri güncellemesinde burayı çağırıyor;\n3155 |     //   pencere modunda bu, her `state` mesajında boşuna bir palet JSON'u\n3156 |     //   üretip kanal yükü yaratıyordu.\n3157 |     if (_cardWindowMode) return;\n3158 |     if (!_palette.enabled) return;\n3159 |     // 🎯 SEÇİCİLİK (PERF — 35. oturum): `_safeNotify` HER veri değişiminde\n3160 |     //   bu metotu çağırıyor ama palet içeriğinin çoğu YALNIZCATEN değil.\n3161 |     //   Asıl maliyet `pushState` içindeki payload inşa + JSON hash; bunu\n3162 |     //   gereksiz her bildirimde tekrar hesaplamak yerine önce palet-\n3163 |     //   İLİŞKİLİ verinin değişmediğini ucuz bir imzadan teyit ederiz.\n3164 |     //   İmza aynıysa payload hiç inşa edilmez (karenin en pahalı parçası atlar).\n3165 |     //\n3166 |     // İmza alanları — palette payload'unu oluşturmayan tek kaynaklar:\n3167 |     //  * [_palette] nesne referansı (değil `==`): Settings copyWith her sefer\n3168 |     //    yeni nesne üretir, bir alan değişmediyse bile referans değişebilir ama\n3169 |     //    aynı değerle tekrar eşleşirse imza aynı kalır (asıl amacımız bu).\n3170 |     //  * [_desktopList] takipleri: masaüstü listesi → desktops[] alanı.\n3171 |     //  * [_macros] sayısı: entries[] ve tiles[] hesaplarının temeli.\n3172 |     final deskSig = _deskSigOf(_desktopList);\n3173 |     final prevSig = _paletteStateSig;\n3174 |     final deskSig = _deskSigOf(_desktopList);\n3175 |     final prevSig = _paletteStateSig;\n3176 |     final nowSig = '${_palette.hashCode}|${_macros.length}|'\n3177 |         '$deskSig|${_paletteTargetId ?? '-'}|${_activePid ?? 0}';\n3178 |     if (prevSig == nowSig) return;\n3179 |     _paletteStateSig = nowSig;\n3180 | \n3181 |     final ctx = paletteContext;\n3182 |     final t = paletteTarget;\n3183 |     final visible = paletteVisibleEntries(_macros, ctx);\n3184 |     // TEŞHİS (seçim tipi filtresi sorunu): her push'ta bağlam + eşleşen\n3185 |     // öğe sayısı/adları log'a yazılır (fikatia_debug.log). Kalıcı — throttle\n3186 |     // yok, palet push'u zaten native JSON imzasıyla dedup edildiği için\n3187 |     // pratikte sık tetiklenmiyor.\n3188 |     debugPrint(\n3189 |       'PALET-PUSH: $ctx hedef=${t?.id ?? '-'} '\n3190 |       '-> ${visible.length}/${_macros.where((m) => m.inPalette).length} öğe '\n3191 |       '[${visible.map((m) => (m.label.isEmpty ? m.name : m.label) + (m.paletteSelectionTypes.isEmpty ? '' : ':${m.paletteSelectionTypes}')).join(', ')}]',\n3192 |     );\n3193 |     unawaited(\n3194 |       PaletteService.pushState(\n3195 |         PaletteStatePayload(\n3196 |           entries: [\n3197 |             for (final m in visible)\n3198 |               PaletteEntryView(\n3199 |                 id: m.id,\n3200 |                 emoji: m.emoji,\n3201 |                 label: m.label.isEmpty ? m.name : m.label,\n3202 |                 tooltip: m.isCommand\n3203 |                     ? 'Komut: ${m.command}'\n3204 |                     : (m.description.isEmpty ? m.name : m.description),\n3205 |                 enabled: t != null,\n3206 |               ),\n3207 |           ],\n3208 |           workbenchId: ctx.workbenchId,\n3209 |           docKind: ctx.docKind,\n3210 |           targetTitle: t?.title ?? '',\n3211 |           note: t == null ? 'bağlı CATIA yok' : '',\n3212 |           viewMode: _palette.viewMode,\n3213 |           showMode: _palette.showMode,\n3214 |           targetPinned: _paletteTargetPinned,\n3215 |           locked: _palette.locked,\n3216 |           hidden: _palette.hidden,\n3217 |           cards: _palette.cards,\n3218 |           // Kart listesi YALNIZ kart görünümü açıkken hesaplanır/kanaldan geçer\n3219 |           // (kapalıyken imza sade kalır, gereksiz trafik olmaz).\n3220 |           tiles: _palette.cards ? paletteTileViews : const <PaletteTileView>[],\n3221 |           // 🖥 Masaüstü listesi yalnız kart görünümü açıkken: kapalıyken hiçbir\n3222 |           // kartta 🖥 düğmesi yok, listeyi göndermek boş trafik olurdu.\n3223 |           desktops: _palette.cards\n3224 |               ? _paletteDesktops\n3225 |               : const <PaletteDesktopView>[],\n3226 |         ),\n3227 |       ),\n3228 |     );\n3229 |   }\n3230 | \n3231 |   // ---- 🗂 Palet KART görünümü (Aşama 1) ----\n3232 | \n3233 |   /// ✏️ Palet kartından gelen \"yeniden adlandır\" isteği. Palet penceresi\n3234 |   /// NOACTIVATE'dir → KLAVYE GİRİŞİ YOKTUR (palette_screen.dart I6 notu), bu\n3235 |   /// yüzden metin diyaloğu orada açılamaz: istek buraya gelir, popup öne\n3236 |   /// alınır ve ARAYÜZ diyaloğu açar (bkz. home_screen.dart `_TileRenameBridge`).\n3237 |   /// Arayüz diyaloğu kapattığında değer null'a çekilir.\n3238 |   final ValueNotifier<PaletteTileRenameRequest?> tileRenameRequest =\n3239 |       ValueNotifier(null);\n3240 | \n3241 |   /// 🖥✏️ Aynı düzende **masaüstü adı** isteği (Aşama 2). Palet NOACTIVATE →\n3242 |   /// alt pencerede metin alınamaz; diyalog ana pencerede açılır.\n3243 |   final ValueNotifier<PaletteDesktopRenameRequest?> tileDesktopRenameRequest =\n3244 |       ValueNotifier(null);\n3245 | \n3246 |   /// Palet kart görünümünün çizeceği kartlar — ana penceredeki liste ile AYNI\n3247 |   /// kaynak ([_cardInstances]) ve AYNI kurallar. Masaüstü etiketi, aktif\n3248 |   /// pencere vurgusu ve workbench rozeti burada çözülür; alt pencere registry/\n3249 |   /// DPI/tema mantığını hiç bilmez (tek karar kaynağı korunur).\n3250 |   ///\n3251 |   /// 📌 SIRALAMA: **hedef kart her zaman ilk sırada** (bkz. [_tileTargetFirst]).\n3252 |   ///\n3253 |   /// 🎯 YALNIZ HEDEF KART: palet pencerisi TEK kart çizer — **aktif (🎯 hedef)\n3254 |   /// CATIA'nın kartı** — ve o kart komut ızgarasının ÜSTÜNDE durur. Geri\n3255 |   /// kalan CATIA'lar listede BİLEREK gönderilmez: palet küçük bir yardımcı\n3256 |   /// pencere, odak her an başka bir CATIA'da olabilir. Başka kartı görmek/\n3257 |   /// yönetmek için ana pencere (ya da başlıktaki 🎯 ile hedef değiştirme).\n3258 |   ///\n3259 |   /// Hedef yoksa (palete bağlı CATIA yok) liste BOŞ gider → palet \"_Empty\"\n3260 |   /// yerine kart gövdesi boş kalır, kullanıcı yanlış kartta komut çalıştırmaz.\n3261 |   List<PaletteTileView> get paletteTileViews {\n3262 |     final target = paletteTarget;\n3263 |     if (target == null) return const [];\n3264 |     final i = target;\n3265 |     final effectivePid = realWindowPid(i.id) ?? i.pid;\n3266 |     return [\n3267 |       PaletteTileView(\n3268 |         id: i.id,\n3269 |         title: i.title,\n3270 |         caption: i.caption,\n3271 |         hasCustomTitle: i.hasCustomTitle,\n3272 |         fileName: i.fileName,\n3273 |         status: i.status,\n3274 |         desktopNumber: i.desktopNumber ?? 0,\n3275 |         desktopName: i.desktopName ?? '',\n3276 |         desktopLabel: desktopLabel(\n3277 |           i.desktopGuid,\n3278 |           desktopNumber: i.desktopNumber,\n3279 |         ),\n3280 |         workbenchId: i.workbenchId,\n3281 |         selectionCount: i.selectionCount,\n3282 |         pid: i.pid,\n3283 |         placeholder: i.placeholder,\n3284 |         openUnconnected: i.isOpenUnconnected,\n3285 |         connected: i.connected,\n3286 |         // 👻 kartın karşılığı bir pencere yok → hiç vurgulanmaz (ana karttaki\n3287 |         // kuralla aynı).\n3288 |         activeWindow:\n3289 |             !i.placeholder && effectivePid > 0 && effectivePid == _activePid,\n3290 |         scriptBusy: isScriptBusy(i.id),\n3291 |         progress: i.progress ?? -1,\n3292 |         // 🖥 Windows'un o an bulunulan masaüstü: CATIA'nınkiyle AYNI mi?\n3293 |         // Odak bilgisi (_activePid) BUNUNLA karıştırılmaz — odak, pencerenin\n3294 |         // nerede DEĞİL nerede olduğunu söylemez.\n3295 |         onCurrentDesktop: _isOnCurrentDesktop(i),\n3296 |         macros: _tileMacrosFor(i),\n3297 |         docs: [\n3298 |           for (final d in i.documents)\n3299 |             PaletteTileDocView(\n3300 |               id: d.id,\n3301 |               name: d.name,\n3302 |               saved: d.saved,\n3303 |               isActive: d.isActive,\n3304 |             ),\n3305 |         ],\n3306 |         closedDocs: [\n3307 |           for (final d in closedDocumentsFor(i.id))\n3308 |             PaletteTileDocView(id: d.name, name: d.name, closed: true),\n3309 |         ],\n3310 |       ),\n3311 |     ];\n3312 |   }\n3313 | \n3314 |   /// 🪟 Bu kartın pencereye gönderilecek TAM verisi (JSON).\n3315 |   ///\n3316 |   /// 📌 NEDEN `paletteTileViews` DEĞİL: o getter **hedef kartı** (🎯) döndürür\n3317 |   /// (palet tek kart çizer). Aktarılan kart hangisi olursa olsun **o** kartın\n3318 |   /// verisi gitmeli → model doğrudan bu instance için kurulur.\n3319 |   ///\n3320 |   /// Kart listeden KALDIRILIRSA (❌ / CATIA kapandı) null döner; çağıran taraf\n3321 |   /// pencereye \"kart yok\" bildirip pencereyi kapatır.\n3322 |   PaletteTileView? tileViewFor(String instanceId) {\n3323 |     final i = _cardInstances.where((x) => x.id == instanceId).firstOrNull;\n3324 |     if (i == null) return null;\n3325 |     final effectivePid = realWindowPid(i.id) ?? i.pid;\n3326 |     return PaletteTileView(\n3327 |       id: i.id,\n3328 |       title: i.title,\n3329 |       caption: i.caption,\n3330 |       hasCustomTitle: i.hasCustomTitle,\n3331 |       fileName: i.fileName,\n3332 |       status: i.status,\n3333 |       desktopNumber: i.desktopNumber ?? 0,\n3334 |       desktopName: i.desktopName ?? '',\n3335 |       desktopLabel: desktopLabel(i.desktopGuid, desktopNumber: i.desktopNumber),\n3336 |       workbenchId: i.workbenchId,\n3337 |       selectionCount: i.selectionCount,\n3338 |       pid: i.pid,\n3339 |       placeholder: i.placeholder,\n3340 |       openUnconnected: i.isOpenUnconnected,\n3341 |       connected: i.connected,\n3342 |       activeWindow:\n3343 |           !i.placeholder && effectivePid > 0 && effectivePid == _activePid,\n3344 |       scriptBusy: isScriptBusy(i.id),\n3345 |       progress: i.progress ?? -1,\n3346 |       onCurrentDesktop: _isOnCurrentDesktop(i),\n3347 |       macros: _tileMacrosFor(i),\n3348 |       docs: [\n3349 |         for (final d in i.documents)\n3350 |           PaletteTileDocView(\n3351 |             id: d.id,\n3352 |             name: d.name,\n3353 |             saved: d.saved,\n3354 |             isActive: d.isActive,\n3355 |           ),\n3356 |       ],\n3357 |       closedDocs: [\n3358 |         for (final d in closedDocumentsFor(i.id))\n3359 |           PaletteTileDocView(id: d.name, name: d.name, closed: true),\n3360 |       ],\n3361 |     );\n3362 |   }\n3363 | \n3364 |   /// 🪟 Kartı bağımsız pencereye AÇAR (kartın 🗖 düğmesi).\n3365 |   ///\n3366 |   /// Pencere \"tüm sanal masaüstlerinde\" çalışır, taşınabilir ve boyutlandırılabilir\n3367 |   /// (bkz. `CardWindowService.open` → `NativeBridge.setupCardWindow`).\n3368 |   /// Aynı kart için zaten pencere varsa yenisi açılmaz, o öne getirilir.\n3369 |   /// 🪟 Bu kartın aktarılmış penceresi şu an AÇIK mı? (düğmenin rengi bununla\n3370 |   /// değişir; kart widget'ı sadece BİLİR, karar vermez).\n3371 |   bool isCardWindowOpen(String id) => CardWindowService.isOpen(id);\n3372 | \n3373 |   /// 🧪 Yalnızca `--selftest-card` için: kart listesindeki N. kartın id'si.\n3374 |   ///\n3375 |   /// 📌 `openCardWindow` kartı `_cardInstances` içinde arıyor; selftest\n3376 |   ///   `instances` (filtrelenmiş liste) üzerinden seçince ikinci/üçüncü kart\n3377 |   ///   \"bulunamadı\" deyip 1 ms'de dönüyordu. Bu yardımcı aynı listeyi\n3378 |   ///   kullanır.\n3379 |   ///\n3380 |   /// 📌 `public` olmalı: `main.dart` BAŞKA dosyadan çağırıyor (Dart'ta\n3381 |   ///   alt çizgili üyeler dosya kapsamlıdır).\n3382 |   String? debugCardIdAt(int index) {\n3383 |     final list = _cardInstances;\n3384 |     if (index < 0 || index >= list.length) return null;\n3385 |     return list[index].id;\n3386 |   }\n3387 | \n3388 |   /// 🪟 Bu kartın aktarılmış penceresini açar.\n3389 |   Future<bool> openCardWindow(String instanceId) async {\n3390 |     final inst = _cardInstances.where((i) => i.id == instanceId).firstOrNull;\n3391 |     if (inst == null) {\n3392 |       _setSessionMessage('🪟 Kart bulunamadı', warn: true);\n3393 |       return false;\n3394 |     }\n3395 |     final ok = await CardWindowService.open(inst.id, inst.title);\n3396 |     if (!ok) {\n3397 |       _setSessionMessage('🪟 Pencere açılamadı', warn: true);\n3398 |       return false;\n3399 |     }\n3400 |     _setSessionMessage('🪟 \"${inst.title}\" ayrı pencereye aktarıldı');\n3401 |     // Canlı doku varsa durdur: pencereye kare PNG yoluyla akacak.\n3402 |     if (_textureIds.containsKey(instanceId)) {\n3403 |       await _stopLiveThumbTexture(instanceId);\n3404 |       _syncLiveThumbTimer();\n3405 |       unawaited(_captureLiveBatch());\n3406 |     }\n3407 |     // 🪟 YENİ PENCERE = yeni alıcı: o kareyi bu pencere bilmiyor, bu yüzden\n3408 |     //   görüntü ilk gönderimde ZORLA taşınmalı (aksi halde imza filtresi onu\n3409 |     //   eler ve pencere boş kalır).\n3410 |     _cardWindowThumbForce.add(instanceId);\n3411 |     // İlk veriyi hemen gönder (pencere hazır olmadan boş kalmasın).\n3412 |     // 📌 Servis `ready` kapısı varken bu çağrı veriyi `_lastState`'te\n3413 |     //   bekletir; `ready` gelince gönderilir.\n3414 |     unawaited(_pushCardWindowState(instanceId));\n3415 |     return true;\n3416 |   }\n3417 | \n3418 |   // -----------------------------------------------------------------------\n3419 |   // 🗂️ CANLI GÖRÜNÜM PENCERE YÖNETİCİSİ (minimal, ayrı pencere)\n3420 |   // -----------------------------------------------------------------------\n3421 |   //\n3422 |   // İSTEK (02.10.2026): kayıtlı (konumu hatırlanan) canlı görünüm pencereleri\n3423 |   // için ayrı bir yönetici; CATIA'sı bağlı olanları tek tek ya da toplu aç/kapat.\n3424 |   // Pencere arayüzü: lib/screens/window_manager_screen.dart. Tek yazıcı bu\n3425 |   // sınıftır; yönetici pencere yalnız istek yollar (bkz. WindowManagerService).\n3426 | \n3427 |   /// 🗂️ Pencere yöneticisini açar (Araçlar ▸ Pencere Yöneticisi).\n3428 |   Future<void> openWindowManager() async {\n3429 |     try {\n3430 |       await WindowManagerService.open(\n3431 |         entries: windowManagerEntries,\n3432 |         openOne: openRegisteredCardWindow,\n3433 |         closeOne: closeRegisteredCardWindow,\n3434 |         arrange: arrangeCardWindows,\n3435 |       );\n3436 |     } catch (e) {\n3437 |       _setSessionMessage('Pencere yöneticisi açılamadı: $e', warn: true);\n3438 |     }\n3439 |   }\n3440 | \n3441 |   /// 🧱 AÇIK canlı görünüm pencerelerini sağ üst köşeye küçük kart ızgarası\n3442 |   /// olarak DİZER (yönetici ▸ \"Diz\"). Boyut/konum hesabı saf fonksiyonda\n3443 |   /// ([tileGridTopRight]); burada yalnız ekran çalışma alanı okunur ve her\n3444 |   /// pencereye uygulanır. Uygulanan konum, konum defterine 3 sn içinde\n3445 |   /// kendiliğinden işlenir (pencere bir daha açılınca aynı yerde gelir).\n3446 |   /// Dönen sayı: dizilen pencere adedi.\n3447 |   Future<int> arrangeCardWindows(List<String> ids) async {\n3448 |     final open = [\n3449 |       for (final id in ids)\n3450 |         if (CardWindowService.isOpen(id)) id,\n3451 |     ];\n3452 |     if (open.isEmpty) return 0;\n3453 |     final display = await screenRetriever.getPrimaryDisplay();\n3454 |     final sf = (display.scaleFactor ?? 1.0).toDouble();\n3455 |     final size = display.visibleSize ?? display.size;\n3456 |     // screen_retriever mantıksal birim verir; pencereler FİZİKSEL px ister.\n3457 |     final work = WindowRect(\n3458 |       ((display.visiblePosition?.dx ?? 0) * sf).round(),\n3459 |       ((display.visiblePosition?.dy ?? 0) * sf).round(),\n3460 |       (size.width * sf).round(),\n3461 |       (size.height * sf).round(),\n3462 |     );\n3463 |     final rects = tileGridTopRight(count: open.length, work: work);\n3464 |     for (var i = 0; i < open.length; i++) {\n3465 |       await CardWindowService.applyRect(open[i], rects[i]);\n3466 |     }\n3467 |     return open.length;\n3468 |   }\n3469 | \n3470 |   /// 🗂️ Kayıtlı oturumdaki \"pencere kaydı olan\" CATIA'ları, o an bağlı\n3471 |   /// CATIA'larla EŞLER (TAŞIMASIZ, durum DEĞİŞTİRMEZ).\n3472 |   ///\n3473 |   /// Eşleşme: önce ortak açık döküman yolu (restoreSession'daki [_matchLive]\n3474 |   /// ile aynı ölçüt, 1'e 1 ve en çok ortak dökümana göre), sonra özel başlık\n3475 |   /// eşitliği. Eşleşmeyen kayıt `live: null` döner (CATIA bağlı değil).\n3476 |   List<({SavedInstance saved, CatiaInstance? live})> _wmSavedPairs() {\n3477 |     final session = _savedSession;\n3478 |     if (session == null) return const [];\n3479 |     final saved = <SavedInstance>[\n3480 |       for (final s in session.instances)\n3481 |         if (s.cardWindowOpen || s.cardWindowRect != null) s,\n3482 |     ];\n3483 |     if (saved.isEmpty) return const [];\n3484 |     final live = _liveInstances;\n3485 |     final livePaths = [\n3486 |       for (final l in live)\n3487 |         {\n3488 |           for (final d in l.documents)\n3489 |             if (d.path.isNotEmpty) '${d.path}${d.name}'.toLowerCase(),\n3490 |         },\n3491 |     ];\n3492 |     final pairs = <({int score, int li, int si})>[];\n3493 |     for (var li = 0; li < live.length; li++) {\n3494 |       for (var si = 0; si < saved.length; si++) {\n3495 |         final score = saved[si].documents\n3496 |             .where((d) => livePaths[li].contains(d.fullPath.toLowerCase()))\n3497 |             .length;\n3498 |         if (score > 0) pairs.add((score: score, li: li, si: si));\n3499 |       }\n3500 |     }\n3501 |     pairs.sort((a, b) => b.score.compareTo(a.score));\n3502 |     final liveOf = <int, int>{};\n3503 |     final usedLive = <int>{};\n3504 |     for (final p in pairs) {\n3505 |       if (liveOf.containsKey(p.si) || usedLive.contains(p.li)) continue;\n3506 |       liveOf[p.si] = p.li;\n3507 |       usedLive.add(p.li);\n3508 |     }\n3509 |     // İkinci tur: dökümanı değişmiş olsa bile ÖZEL adı aynı olan CATIA.\n3510 |     for (var si = 0; si < saved.length; si++) {\n3511 |       if (liveOf.containsKey(si)) continue;\n3512 |       final t = saved[si].title.trim().toLowerCase();\n3513 |       if (t.isEmpty || t == saved[si].id.trim().toLowerCase()) continue;\n3514 |       for (var li = 0; li < live.length; li++) {\n3515 |         if (usedLive.contains(li)) continue;\n3516 |         if (live[li].title.trim().toLowerCase() == t) {\n3517 |           liveOf[si] = li;\n3518 |           usedLive.add(li);\n3519 |           break;\n3520 |         }\n3521 |       }\n3522 |     }\n3523 |     return [\n3524 |       for (var si = 0; si < saved.length; si++)\n3525 |         (\n3526 |           saved: saved[si],\n3527 |           live: liveOf[si] == null ? null : live[liveOf[si]!],\n3528 |         ),\n3529 |     ];\n3530 |   }\n3531 | \n3532 |   /// 🗂️ Yönetici listesi: konumu kayıtlı her canlı görünüm penceresi.\n3533 |   ///\n3534 |   /// Kaynaklar (birleşik): kayıtlı oturum + kalıcı konum defteri + şu an açık\n3535 |   /// pencereler. CATIA'sı bağlı olanın `liveId`'si doludur.\n3536 |   Future<List<WmEntry>> windowManagerEntries() async {\n3537 |     await CardRectStore.ensureLoaded();\n3538 |     final out = <WmEntry>[];\n3539 |     final used = <String>{};\n3540 | \n3541 |     String sub(String desktop, String doc, WindowRect? r) => [\n3542 |       if (desktop.isNotEmpty && desktop != 'M?') desktop,\n3543 |       if (doc.isNotEmpty) doc,\n3544 |       if (r != null && r.isUsable) '${r.w}×${r.h}',\n3545 |     ].join(' · ');\n3546 | \n3547 |     for (final p in _wmSavedPairs()) {\n3548 |       final s = p.saved;\n3549 |       final l = p.live;\n3550 |       if (l != null) used.add(l.id);\n3551 |       final rect = l == null\n3552 |           ? s.cardWindowRect\n3553 |           : (CardWindowService.lastRectOf(l.id) ??\n3554 |                 CardRectStore.lookup(l.id, l.title) ??\n3555 |                 s.cardWindowRect);\n3556 |       final doc = l != null\n3557 |           ? (l.documents.where((d) => d.isActive).firstOrNull?.name ??\n3558 |                 l.fileName)\n3559 |           : (s.documents.where((d) => d.isActive).firstOrNull?.name ??\n3560 |                 s.documents.firstOrNull?.name ??\n3561 |                 '');\n3562 |       out.add(\n3563 |         WmEntry(\n3564 |           key: l?.id ?? 'saved:${s.id}',\n3565 |           title: l?.title ?? s.title,\n3566 |           subtitle: sub(\n3567 |             l != null && l.desktopNumber != null\n3568 |                 ? 'M${l.desktopNumber}'\n3569 |                 : s.desktopLabel,\n3570 |             doc,\n3571 |             rect,\n3572 |           ),\n3573 |           liveId: l?.id ?? '',\n3574 |           open: l != null && CardWindowService.isOpen(l.id),\n3575 |         ),\n3576 |       );\n3577 |     }\n3578 | \n3579 |     for (final l in _liveInstances) {\n3580 |       if (used.contains(l.id)) continue;\n3581 |       final rect =\n3582 |           CardWindowService.lastRectOf(l.id) ??\n3583 |           CardRectStore.lookup(l.id, l.title);\n3584 |       final isOpen = CardWindowService.isOpen(l.id);\n3585 |       if (rect == null && !isOpen) continue; // pencere kaydı yok\n3586 |       out.add(\n3587 |         WmEntry(\n3588 |           key: l.id,\n3589 |           title: l.title,\n3590 |           subtitle: sub(\n3591 |             l.desktopNumber != null ? 'M${l.desktopNumber}' : '',\n3592 |             l.documents.where((d) => d.isActive).firstOrNull?.name ??\n3593 |                 l.fileName,\n3594 |             rect,\n3595 |           ),\n3596 |           liveId: l.id,\n3597 |           open: isOpen,\n3598 |         ),\n3599 |       );\n3600 |     }\n3601 | \n3602 |     // Kararlı sıra: bağlı olanlar önce, sonra ada göre (satırlar zıplamasın).\n3603 |     out.sort((a, b) {\n3604 |       if (a.connected != b.connected) return a.connected ? -1 : 1;\n3605 |       return a.title.toLowerCase().compareTo(b.title.toLowerCase());\n3606 |     });\n3607 |     return out;\n3608 |   }\n3609 | \n3610 |   /// 🗂️ Bağlı CATIA'nın canlı görünüm penceresini KAYITLI konumda açar.\n3611 |   ///\n3612 |   /// Konum sırası ([CardWindowService.open] içinde): bu çalışmadaki son konum >\n3613 |   /// kalıcı defter. İkisi de yoksa ve kayıtlı oturumda konum varsa, o konum\n3614 |   /// önce hafızaya tohumlanır (daha yeni bir hafızayı EZMEZ).\n3615 |   /// CATIA bağlı değilse / pencere zaten açıksa false döner (iş yapılmaz).\n3616 |   Future<bool> openRegisteredCardWindow(String liveId) async {\n3617 |     final live = _liveInstances.where((i) => i.id == liveId).firstOrNull;\n3618 |     if (live == null) return false;\n3619 |     if (CardWindowService.isOpen(live.id)) return true;\n3620 |     try {\n3621 |       await CardRectStore.ensureLoaded();\n3622 |       if (CardWindowService.lastRectOf(live.id) == null &&\n3623 |           CardRectStore.lookup(live.id, live.title) == null) {\n3624 |         final saved = _wmSavedPairs()\n3625 |             .where((p) => p.live?.id == live.id)\n3626 |             .firstOrNull\n3627 |             ?.saved;\n3628 |         CardWindowService.rememberRect(live.id, saved?.cardWindowRect);\n3629 |       }\n3630 |     } catch (e) {\n3631 |       debugPrint('pencere yöneticisi: konum tohumlanamadı ($e)');\n3632 |     }\n3633 |     return openCardWindow(live.id);\n3634 |   }\n3635 | \n3636 |   /// 🗂️ Bağlı CATIA'nın canlı görünüm penceresini kapatır (kart listede KALIR).\n3637 |   Future<bool> closeRegisteredCardWindow(String liveId) async {\n3638 |     if (!CardWindowService.isOpen(liveId)) return false;\n3639 |     await CardWindowService.close(liveId);\n3640 |     return true;\n3641 |   }\n3642 | \n3643 |   /// 📸 Bu kartın EKRAN GÖRÜNTÜSÜ (base64 PNG) — pencere kart gibi görünsün.\n3644 |   ///\n3645 |   /// 📌 TV kartı listede bir EKRAN GÖRÜNTÜSÜ gösterir; pencere de aynı görünmeli.\n3646 |   ///   Görüntü kanal ile gider: alt pencere kendi `CatiaProvider`'ı olmadığı için\n3647 |   ///   `thumbFor`'a erişemez (bkz. `palette_tile.dart` baş notu).\n3648 |   ///\n3649 |   /// 📌 BOYUT: PNG base64 ~1.3 kat büyür. Bu yüzden görüntü **her turda değil,\n3650 |   ///   sadece değiştiğinde** gönderilir (`CardWindowService` imza karşılaştırması).\n3651 |   String thumbBase64(String instanceId) {\n3652 |     final png = thumbFor(instanceId);\n3653 |     if (png == null || png.isEmpty) return '';\n3654 |     return base64Encode(png);\n3655 |   }\n3656 | \n3657 |   /// 🪟 Penceredeki eylemler: bu pencere kendi sahte `_instances`'ıyla çalışır\n3658 |   /// (veri ana pencereden gelir), ama eylem **gerçek** provider'da yürütülmelidir\n3659 |   /// — çünkü `focusInstance`/`saveDocument` gibi işler `_ghosts`, gerçek pid\n3660 |   /// eşlemesi ve COM bağlantısı gerektirir; sahte provider'da bunlar boştur\n3661 |   /// ve düğmeler \"hiçbir şey olmadı\" gibi görünürdü.\n3662 |   ///\n3663 |   /// 📌 Bu yüzden kart penceresindeki TÜM düğmeler kanal üzerinden ana\n3664 |   ///   pencereye gider, orada `handlePaletteTileAction` (listenin ve paletin\n3665 |   ///   kullandığı AYNI kapı) yürütür → \"aynı fonksiyonlar\" birebir sağlanır.\n3666 |   Future<void> actionFocus(String id) async {\n3667 |     _sendToMainWindow(\n3668 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.focus),\n3669 |     );\n3670 |   }\n3671 | \n3672 |   /// 🪟 Başlık değişikliği isteğini ana pencereye gönderir.\n3673 |   ///\n3674 |   /// 📌 [newTitle] kart penceresinde `EditableTitle` ile YAZILAN metindir.\n3675 |   /// Doldurulursa ana pencere diyalog açmadan doğrudan uygular (pencere\n3676 |   /// odak alabiliyor + yazılabiliyor → diyaloğa gerek yok). Boş bırakılırsa\n3677 |   /// ana pencereyi öne alıp yeniden adlandırma diyaloğu açılır.\n3678 |   Future<void> actionRename(String id, [String newTitle = '']) async {\n3679 |     _sendToMainWindow(\n3680 |       PaletteTileRequest(\n3681 |         instanceId: id,\n3682 |         action: PaletteTileAction.rename,\n3683 |         newTitle: newTitle,\n3684 |       ),\n3685 |     );\n3686 |   }\n3687 | \n3688 |   Future<void> actionRestart(String id) async {\n3689 |     _sendToMainWindow(\n3690 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.restart),\n3691 |     );\n3692 |   }\n3693 | \n3694 |   Future<void> actionQuit(String id) async {\n3695 |     _sendToMainWindow(\n3696 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.quit),\n3697 |     );\n3698 |   }\n3699 | \n3700 |   Future<void> actionRemove(String id) async {\n3701 |     _sendToMainWindow(\n3702 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.remove),\n3703 |     );\n3704 |   }\n3705 | \n3706 |   Future<void> actionSaveDoc(String id, String docId) async {\n3707 |     _sendToMainWindow(\n3708 |       PaletteTileRequest(\n3709 |         instanceId: id,\n3710 |         action: PaletteTileAction.saveDoc,\n3711 |         docId: docId,\n3712 |       ),\n3713 |     );\n3714 |   }\n3715 | \n3716 |   Future<void> actionActivateDoc(String id, String docId) async {\n3717 |     _sendToMainWindow(\n3718 |       PaletteTileRequest(\n3719 |         instanceId: id,\n3720 |         action: PaletteTileAction.activateDoc,\n3721 |         docId: docId,\n3722 |       ),\n3723 |     );\n3724 |   }\n3725 | \n3726 |   Future<void> actionOpenDocFolder(String id, String docId) async {\n3727 |     _sendToMainWindow(\n3728 |       PaletteTileRequest(\n3729 |         instanceId: id,\n3730 |         action: PaletteTileAction.openDocFolder,\n3731 |         docId: docId,\n3732 |       ),\n3733 |     );\n3734 |   }\n3735 | \n3736 |   Future<void> actionCloseDoc(String id, String docId) async {\n3737 |     _sendToMainWindow(\n3738 |       PaletteTileRequest(\n3739 |         instanceId: id,\n3740 |         action: PaletteTileAction.closeDoc,\n3741 |         docId: docId,\n3742 |       ),\n3743 |     );\n3744 |   }\n3745 | \n3746 |   Future<void> actionMoveToDesktop(String id, int index, String name) async {\n3747 |     _sendToMainWindow(\n3748 |       PaletteTileRequest(\n3749 |         instanceId: id,\n3750 |         action: PaletteTileAction.moveToDesktop,\n3751 |         desktopIndex: index,\n3752 |         desktopName: name,\n3753 |       ),\n3754 |     );\n3755 |   }\n3756 | \n3757 |   Future<void> actionRunMacro(String id, String macroId) async {\n3758 |     _sendToMainWindow(\n3759 |       PaletteTileRequest(\n3760 |         instanceId: id,\n3761 |         action: PaletteTileAction.runMacro,\n3762 |         macroId: macroId,\n3763 |       ),\n3764 |     );\n3765 |   }\n3766 | \n3767 |   // ---- Kart penceresinde ana pencereye yönlendirilen EK eylemler ----\n3768 |   Future<void> actionToggleLive(String id) async {\n3769 |     _sendToMainWindow(\n3770 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.toggleLive),\n3771 |     );\n3772 |   }\n3773 | \n3774 |   Future<void> actionReopenClosedDoc(\n3775 |     String id,\n3776 |     String name,\n3777 |     String path,\n3778 |   ) async {\n3779 |     _sendToMainWindow(\n3780 |       PaletteTileRequest(\n3781 |         instanceId: id,\n3782 |         action: PaletteTileAction.reopenClosedDoc,\n3783 |         docName: name,\n3784 |         docPath: path,\n3785 |       ),\n3786 |     );\n3787 |   }\n3788 | \n3789 |   Future<void> actionForgetClosedDoc(\n3790 |     String id,\n3791 |     String name,\n3792 |     String path,\n3793 |   ) async {\n3794 |     _sendToMainWindow(\n3795 |       PaletteTileRequest(\n3796 |         instanceId: id,\n3797 |         action: PaletteTileAction.forgetClosedDoc,\n3798 |         docName: name,\n3799 |         docPath: path,\n3800 |       ),\n3801 |     );\n3802 |   }\n3803 | \n3804 |   Future<void> actionRenameDesktop(String id) async {\n3805 |     _sendToMainWindow(\n3806 |       PaletteTileRequest(\n3807 |         instanceId: id,\n3808 |         action: PaletteTileAction.renameDesktop,\n3809 |       ),\n3810 |     );\n3811 |   }\n3812 | \n3813 |   Future<void> actionSwitchDesktop(String id, String name) async {\n3814 |     _sendToMainWindow(\n3815 |       PaletteTileRequest(\n3816 |         instanceId: id,\n3817 |         action: PaletteTileAction.switchToItsDesktop,\n3818 |         desktopName: name,\n3819 |       ),\n3820 |     );\n3821 |   }\n3822 | \n3823 |   Future<void> actionMoveNamed(String id, String name) async {\n3824 |     _sendToMainWindow(\n3825 |       PaletteTileRequest(\n3826 |         instanceId: id,\n3827 |         action: PaletteTileAction.moveToDesktop,\n3828 |         desktopName: name,\n3829 |       ),\n3830 |     );\n3831 |   }\n3832 | \n3833 |   /// 🪟 PENCERE MODU: ana pencereden gelen JSON ile TEK kartı yükler.\n3834 |   ///\n3835 |   /// 📌 AMAÇ: Alt pencere gerçek `CatiaListTile` widget'ını çizsin. O widget\n3836 |   ///   `CatiaInstance` bekler; bu metot kanal JSON'unu (`PaletteTileView`\n3837 |   ///   alanları) tam bir `CatiaInstance`'a çevirir. Böylece pencere, listedeki\n3838 |   ///   kartla **aynı veriyle** çizilir — kopya bir kart değil.\n3839 |   ///\n3840 |   /// 📌 `thumb` (base64 PNG) ayrıca `_thumbs`'a yazılır: `CatiaListTile` görüntüyü\n3841 |   ///   `thumbFor(id)` ile okur (bkz. `catia_list_tile.dart` `_buildThumb`).\n3842 |   // PENCLER KOPRUSU: eylem istegini ANA PENCEREYE gonder.\n3843 |   //\n3844 |   // NEDEN `CardWindowService.onTileAction` DEGIL: o geri cagirma yalniz\n3845 |   // ANA pencerede baglanir (constructor). Alt pencerede NULL'dur ve\n3846 |   // `?.call` sessizce hicbir sey yapmaz -> \"dikmeler calismiyor\".\n3847 |   void _sendToMainWindow(PaletteTileRequest req) {\n3848 |     if (_cardWindowMode) {\n3849 |       unawaited(CardWindowService.sendTileFromChild(req));\n3850 |     }\n3851 |   }\n3852 | \n3853 |   void applyCardWindowState(Map<String, dynamic> json, String thumbB64) {\n3854 |     if (!_cardWindowMode) return;\n3855 |     final id = (json['id'] is String) ? json['id'] as String : '';\n3856 |     if (_instances.isEmpty) {\n3857 |       debugPrint(\n3858 |         'CARD-APPLY: ilk state id=\"$id\" gone=${json['gone']} '\n3859 |         'anahtarlar=${json.keys.length}',\n3860 |       );\n3861 |     }\n3862 |     if (id.isEmpty) return;\n3863 | \n3864 |     // 🛡️ PENCERE MODU SAVUNMASI: kart listeden kalkarsa (❌ düğmesi, CATIA\n3865 |     //   kapandı) ana pencere `null` gönderir. O durumda pencereyi kapatmadan\n3866 |     //   önce KART ARTIK YOKTUR — ama liste boşalınca ekran \"veri bekleniyor\"\n3867 |     //   ekranına düşer ve kullanıcı kartın **listedeki kopyasına geri\n3868 |     //   döndüğünü** sanır. Bu yüzden burada kart yoksa `_instances` BOŞ\n3869 |     //   bırakılır ve ana pencere pencereyi kapatır.\n3870 |     if (json['gone'] == true) {\n3871 |       _instances = <CatiaInstance>[];\n3872 |       notifyListeners();\n3873 |       return;\n3874 |     }\n3875 | \n3876 |     CatiaStatus status;\n3877 |     try {\n3878 |       status = CatiaStatus.values.byName(\n3879 |         (json['status'] is String) ? json['status'] as String : 'idle',\n3880 |       );\n3881 |     } catch (_) {\n3882 |       status = CatiaStatus.idle;\n3883 |     }\n3884 |     int intOr(String k, [int d = 0]) =>\n3885 |         (json[k] is num) ? (json[k] as num).toInt() : d;\n3886 | \n3887 |     // 📄 Döküman listesi (kanaldan gelen JSON'dan).\n3888 |     final docs = <CatiaDocument>[];\n3889 |     final rawDocs = json['docs'];\n3890 |     if (rawDocs is List) {\n3891 |       for (final d in rawDocs) {\n3892 |         if (d is! Map) continue;\n3893 |         final name = (d['name'] is String) ? d['name'] as String : '';\n3894 |         if (name.isEmpty) continue;\n3895 |         docs.add(\n3896 |           CatiaDocument(\n3897 |             id: (d['id'] is String) ? d['id'] as String : name,\n3898 |             name: name,\n3899 |             path: '',\n3900 |             saved: d['saved'] != false,\n3901 |             isActive: d['isActive'] == true,\n3902 |           ),\n3903 |         );\n3904 |       }\n3905 |     }\n3906 | \n3907 |     final inst = CatiaInstance(\n3908 |       id: id,\n3909 |       fileName: (json['fileName'] is String) ? json['fileName'] as String : '',\n3910 |       status: status,\n3911 |       progress: intOr('progress', -1) >= 0 ? intOr('progress', -1) : null,\n3912 |       uptime: Duration.zero,\n3913 |       pid: intOr('pid'),\n3914 |       worker: 'kart penceresi',\n3915 |       connected: json['connected'] == true,\n3916 |       lastError: null,\n3917 |       caption: (json['caption'] is String) ? json['caption'] as String : '',\n3918 |       desktopGuid: (json['desktopGuid'] is String)\n3919 |           ? json['desktopGuid'] as String\n3920 |           : '',\n3921 |     );\n3922 | \n3923 |     // 📌 `title`/`caption` `copyWith`'te DEĞİL (constructor alanları) → bu yüzden\n3924 |     //   başlık `displayName` üzerinden verilir; `CatiaListTile` başlığı oradan\n3925 |     //   okur. `caption` alanı doğrudan kurucuya aynı değerle verilmiştir.\n3926 |     _instances = [\n3927 |       inst.copyWith(\n3928 |         displayName: (json['title'] is String) ? json['title'] as String : id,\n3929 |         desktopName: (json['desktopName'] is String)\n3930 |             ? json['desktopName'] as String\n3931 |             : null,\n3932 |         desktopNumber: intOr('desktopNumber', 0) > 0\n3933 |             ? intOr('desktopNumber', 0)\n3934 |             : null,\n3935 |         workbenchId: (json['workbenchId'] is String)\n3936 |             ? json['workbenchId'] as String\n3937 |             : '',\n3938 |         selectionCount: intOr('selectionCount'),\n3939 |         selectionTypes: const <String>{},\n3940 |         computing: json['computing'] == true,\n3941 |         documents: docs,\n3942 |       ),\n3943 |     ];\n3944 |     _mockActive = false;\n3945 | \n3946 |     // 🪟 CANLI VERİ (Görev 5): ana pencerenin gönderdiği ek alanlar. Bunlar\n3947 |     // yazılmazsa alt pencerede ▶/🖥/🕘/👻/🔴 düğmeleri boş ya da ölü kalır.\n3948 |     final placeholder = json['placeholder'] == true;\n3949 |     final openUnc = json['openUnconnected'] == true;\n3950 |     final pidNow = intOr('pid');\n3951 |     _activePid = json['activeWindow'] == true\n3952 |         ? (pidNow > 0 ? pidNow : 1)\n3953 |         : null;\n3954 |     if (placeholder || openUnc) {\n3955 |       _instances = [_instances.first.copyWith()]\n3956 |           .map(\n3957 |             (e) => CatiaInstance(\n3958 |               id: e.id,\n3959 |               fileName: e.fileName,\n3960 |               status: e.status,\n3961 |               pid: e.pid,\n3962 |               worker: e.worker,\n3963 |               connected: e.connected,\n3964 |               displayName: e.displayName,\n3965 |               progress: e.progress,\n3966 |               documents: e.documents,\n3967 |               desktopGuid: e.desktopGuid,\n3968 |               desktopNumber: e.desktopNumber,\n3969 |               desktopName: e.desktopName,\n3970 |               caption: e.caption,\n3971 |               workbenchId: e.workbenchId,\n3972 |               selectionCount: e.selectionCount,\n3973 |               computing: e.computing,\n3974 |               placeholder: true,\n3975 |               openPid: openUnc ? 1 : 0,\n3976 |             ),\n3977 |           )\n3978 |           .toList();\n3979 |     }\n3980 |     // ⏳/📊 percent (ilerleme) — `progress` yoksa `percent` kullanılır.\n3981 |     final pct = intOr('percent', -1);\n3982 |     if (pct >= 0 &&\n3983 |         _instances.isNotEmpty &&\n3984 |         _instances.first.progress == null) {\n3985 |       _instances = [_instances.first.copyWith(progress: pct)];\n3986 |     }\n3987 |     // 🔴 canlı görüntü bayrakları\n3988 |     _remoteLive = json['live'] == true;\n3989 |     _remoteLiveOk = json['liveOk'] != false;\n3990 |     // ▶ Makro listesi (ana pencerenin bağlama uyan kayıtları) + meşgul bayrağı\n3991 |     final rawMacros = json['macros'];\n3992 |     _macros = <SavedMacro>[\n3993 |       if (rawMacros is List)\n3994 |         for (final m in rawMacros)\n3995 |           if (m is Map && m['id'] is String && m['label'] is String)\n3996 |             SavedMacro(\n3997 |               id: m['id'] as String,\n3998 |               name: m['label'] as String,\n3999 |               label: m['label'] as String,\n4000 |               path: '',\n4001 |               emoji: (m['emoji'] is String) ? m['emoji'] as String : '',\n4002 |               description: (m['tooltip'] is String)\n4003 |                   ? m['tooltip'] as String\n4004 |                   : '',\n4005 |               // `code`/`command` dolu ⇒ `isCode/isCommand` true ⇒ çalıştırılabilir\n4006 |               // görünür; gerçek yürütme ANA pencerede id ile yapılır.\n4007 |               command: m['isCommand'] == true ? (m['label'] as String) : null,\n4008 |               code: m['isCommand'] == true ? null : '',\n4009 |             ),\n4010 |     ];\n4011 |     _scriptBusy\n4012 |       ..clear()\n4013 |       ..addAll(json['scriptBusy'] == true ? <String>{id} : <String>{});\n4014 |     // 🕘 Geçmiş dökümanlar\n4015 |     final rawClosed = json['closedDocs'];\n4016 |     _closedDocs[id] = [\n4017 |       if (rawClosed is List)\n4018 |         for (final d in rawClosed)\n4019 |           if (d is Map && d['name'] is String)\n4020 |             ClosedCatiaDocument(\n4021 |               name: d['name'] as String,\n4022 |               path: '',\n4023 |               closedAt: DateTime.now(),\n4024 |             ),\n4025 |     ];\n4026 |     // 💬 SORULAR: ana pencereden gelen CATIA diyalogları.\n4027 |     //\n4028 |     // 🐛 29.09.2026 — KULLANICI: \"dışarı sabitlenen pencerede tile card'taki\n4029 |     //   gibi dialog göstermiyor.\" NEDEN: gönderici (`_pushCardWindowState`)\n4030 |     //   `dialogs` alanını taşımıyor, burada da `_dialogsById` doldurulmuyordu.\n4031 |     //   Alt pencerenin kendi `CatiaProvider`'ı bu yüzden `dialogsOf(id)` →\n4032 |     //   boş liste, `hasDialog(id)` → false döndürüyordu; ❓ rozeti çıkmadığı için\n4033 |     //   `_buildDialogs` erkenden `SizedBox.shrink()` dönüyor, panel hiç açılmıyordu.\n4034 |     //   Kart penceresi gerçek `CatiaListTile` çizdiği için veri şarttı.\n4035 |     //\n4036 |     // 📌 DİYALOG BOŞSA KÜME TEMİZLENİR: soru kapandığında eski kayıt kalırsa\n4037 |     //   pencere \"cevaplanmış ama hâlâ görünen\" bir soruyu gösterirdi.\n4038 |     final dialogs = _dialogsFrom(json['dialogs']);\n4039 |     if (dialogs.isEmpty) {\n4040 |       _dialogsById.remove(id);\n4041 |     } else {\n4042 |       _dialogsById[id] = dialogs;\n4043 |     }\n4044 |     // 🖥 Masaüstü listesi + motor\n4045 |     final rawDesk = json['desktops'];\n4046 |     if (rawDesk is List) {\n4047 |       _desktopList = [\n4048 |         for (final d in rawDesk)\n4049 |           if (d is Map)\n4050 |             DesktopInfo(\n4051 |               index: (d['index'] is num) ? (d['index'] as num).toInt() : -1,\n4052 |               name: (d['name'] is String) ? d['name'] as String : '',\n4053 |               visible: d['visible'] == true,\n4054 |               guid: (d['guid'] is String) ? d['guid'] as String : '',\n4055 |             ),\n4056 |       ];\n4057 |     }\n4058 |     final eng = json['desktopEngine'];\n4059 |     if (eng is String) {\n4060 |       _settings = _settings.copyWith(\n4061 |         desktopEngine: eng == 'powershell'\n4062 |             ? DesktopEngine.powershell\n4063 |             : DesktopEngine.native,\n4064 |       );\n4065 |     }\n4066 | \n4067 |     // 📸 Görüntü: kartın ekran görüntüsü (`CatiaListTile` `thumbFor` ile okur).\n4068 |     //\n4069 |     // 🔴 GÖRÜNTÜ DE AYNI İÇERİKTEYSE ELENİR. `base64Decode` her seferinde\n4070 |     //   YENİ bir `Uint8List` üretir; karşılaştırma `identical()` ile yapılsa\n4071 |     //   \"her turda değişmiş\" sayılır ve `_thumbs[id]` her defasında yeni\n4072 |     //   nesneyle değişir → `CatiaListTile` kareyi YENİDEN decode eder,\n4073 |     //   kart tam yeniden çizilir. Bu, komut çalışırken penceredeki titreme\n4074 |     //   ve kasmanın kaynağıydı. Artık İÇERİK hash'i karşılaştırılır.\n4075 |     var thumbChanged = false;\n4076 |     if (thumbB64.isNotEmpty) {\n4077 |       try {\n4078 |         final bytes = base64Decode(thumbB64);\n4079 |         final h = _fingerprintBytes(bytes);\n4080 |         thumbChanged = h != _cardWindowThumbHash[id];\n4081 |         _cardWindowThumbHash[id] = h;\n4082 |         // STAB-9: doğrudan `_thumbs[id] = bytes` 24'lük sınırı ve `_thumbOrder`\n4083 |         // sırasını atlıyordu (uzun çalışmada kart penceresi görüntüleri birikir).\n4084 |         _storeThumb(id, bytes);\n4085 |       } catch (_) {\n4086 |         // Bozuk base64 → görsel yok, kart yine çizilir.\n4087 |         thumbChanged = true;\n4088 |       }\n4089 |     }\n4090 |     // 🔴 ALT PENCEREDE GEREKSİZ ÇİZİM KESİCİ.\n4091 |     //\n4092 |     //   `notifyListeners()` → `CardWindowScreen` `context.watch` ile dinliyor →\n4093 |     //   kartın TÜM ağacı (gradient + blur + gölge + döküman listesi) baştan\n4094 |     //   çiziliyor ve yeni PNG için `_decodeThumb` tetikleniyor.\n4095 |     //\n4096 |     //   Ana pencere artık imza filtresiyle aynı veriyi göndermiyor; yine de\n4097 |     //   savunma olarak burada da \"görüntü yoksa ve metin imzası aynıysa\"\n4098 |     //   çizim yapılmaz.\n4099 |     final sig = _cardWindowSigOf(json);\n4100 |     if (!thumbChanged && sig == _cardWindowStateSig[id]) return;\n4101 |     _cardWindowStateSig[id] = sig;\n4102 |     notifyListeners();\n4103 |   }\n4104 | \n4105 |   /// 🪟 Pencereyi AÇ (görsel test + teşhis). `openCardWindow`'a ek olarak\n4106 |   /// ayrıntılı log yazar: HWND bulundu mu, setupCardWindow başarılı mı,\n4107 |   /// kanal açıldı mı. Kullanıcı \"pencere açılmıyor\" derse bu log tek\n4108 |   /// bakışta nerede koptuğunu söyler.\n4109 |   Future<bool> openCardWindowVerbose(String instanceId) async {\n4110 |     final ok = await openCardWindow(instanceId);\n4111 |     _dlog(\n4112 |       'PENCERE: $instanceId istenildi -> sonuc=$ok '\n4113 |       'kayitli=${CardWindowService.isOpen(instanceId)} '\n4114 |       'pencereSayisi=${CardWindowService.openIds.length}',\n4115 |     );\n4116 |     return ok;\n4117 |   }\n4118 | \n4119 |   /// 🪟 instanceId -> pencereye gönderilmiş SON ham PNG nesnesi.\n4120 |   ///\n4121 |   /// 📌 Amaç: `thumbBase64` çağrısını gereksiz yere yapmamak. `Uint8List`\n4122 |   ///   her yakalamada YENİ bir nesne olduğu için nesne kimliği\n4123 |   ///   (`identical`) güvenilir bir \"kare değişti\" sinyalidir. Aynı nesne ise\n4124 |   ///   kare değişmemiştir → 226 KB'lık PNG encode'u hiç yapılmaz.\n4125 |   final Map<String, Uint8List> _cardWindowThumbRefs = <String, Uint8List>{};\n4126 | \n4127 |   /// 🪟 Bir sonraki gönderimde görüntüyü `thumb` alanıyla ZORLA gönder.\n4128 |   /// (Pencere yeni açıldığında eski imza kaydı hâlâ duruyor olabilir.)\n4129 |   final Set<String> _cardWindowThumbForce = <String>{};\n4130 | \n4131 |   /// 🪟 instanceId -> pencereye gönderilmiş SON durum imzası.\n4132 |   ///\n4133 |   /// 🔴 PERFORMANS (kasma düzeltmesi): `pushState` her çağrıda kanala\n4134 |   /// yazıyordu; oysa komut/makro çalışırken bile kartın gösterilebilir\n4135 |   /// verisi çoğu turda BİREBİR AYNI'dır (durum, başlık, rozet, ilerleme\n4136 |   /// değişmemiştir). Her turda N pencereye tam JSON yazmak:\n4137 |   ///   • ana pencere UI thread'ini `invokeMethod` beklerken kilitler,\n4138 |   ///   • alt pencerede `notifyListeners()` → kartın TAMAMEN yeniden\n4139 |   ///     çizilmesi + PNG decode tetikler,\n4140 |   /// yani \"pencere sayısı × snapshot sıklığı\" ile katlanan bir yüktür.\n4141 |   ///\n4142 |   /// ✅ ÇÖZÜM: gösterilecek alanların kısa bir imzası tutulur; imza\n4143 |   /// değişmediyse gönderim TAMAMEN atlanır (görüntü değiştiyse o da\n4144 |   /// imzaya katılır, yoksa sessizce eskir).\n4145 |   final Map<String, String> _cardWindowStateSig = <String, String>{};\n4146 | \n4147 |   /// 🪟 PNG baytlarının içerik parmak izi (hızlı, çakışma düşük).\n4148 |   ///\n4149 |   /// 📌 NEDEN İÇERİK HASH'İ: `identical()` yalnız NESNE KİMLİĞİ yalar.\n4150 |   /// `_storeThumb` her yakalamada yeni `Uint8List` ürettiği için aynı\n4151 |   /// görüntü bile \"değişmiş\" sayılır → her turda 226 KB base64 encode +\n4152 |   /// kanal trafiği. Bu hash ile AYNI İÇERİK elenir; sadece gerçekten yeni\n4153 |   /// kare gider. (64-bit FNV-1a; 226 KB'ın tamamı taranır, saniyede\n4154 |   /// birkaç kez çalışır — decode/JSON'un yanında ihmal edilebilir.)\n4155 |   static int _fingerprintBytes(Uint8List bytes) {\n4156 |     var h = 0xcbf29ce484222325;\n4157 |     for (var i = 0; i < bytes.length; i++) {\n4158 |       h ^= bytes[i];\n4159 |       h *= 0x100000001b3;\n4160 |     }\n4161 |     // Uzunluk da karışır (aynı hash, farklı boyut olasılığını düşürür).\n4162 |     h ^= bytes.length;\n4163 |     return h & 0x7fffffffffffffff;\n4164 |   }\n4165 | \n4166 |   /// 🪟 instanceId -> son gönderilen görüntünün içerik parmak izi.\n4167 |   final Map<String, int> _cardWindowThumbHash = <String, int>{};\n4168 | \n4169 |   /// 🪟 Kart penceresine gönderilecek durumun kısa imzası.\n4170 |   ///\n4171 |   /// `thumb` BİLEREK dışarıda bırakıldı: görüntü zaten kendi hash'iyle\n4172 |   /// denetleniyor ve 226 KB'lık bir alanı imzalamak pahalı olurdu.\n4173 |   static String _cardWindowSigOf(Map<String, dynamic> payload) {\n4174 |     final keys = payload.keys.toList()..sort();\n4175 |     final sb = StringBuffer();\n4176 |     for (final k in keys) {\n4177 |       if (k == 'thumb') continue;\n4178 |       final v = payload[k];\n4179 |       // Liste/harita değişken uzunlukta olabilir → kimliği değil, kısa\n4180 |       // özet (`jsonEncode`) kullanılır. Büyük listeler (desktops/macros)\n4181 |       // JSON'a çevrildiği için bu da tam maliyet değildir; asıl kazanç\n4182 |       // \"hiç göndermemek\"ten gelir.\n4183 |       sb\n4184 |         ..write(k)\n4185 |         ..write('=')\n4186 |         ..write(v is List || v is Map ? jsonEncode(v).hashCode : '$v')\n4187 |         ..write(';');\n4188 |     }\n4189 |     return sb.toString();\n4190 |   }\n4191 | \n4192 |   // 🖼️ Kartın penceresi açıkken doku kullanılabilir mi? (SEÇENEK B)\n\n[Showing lines 3134-4192 of 10452. Use start_line/end_line to read other sections.]",
              "success": true
            },
            {
              "query": "./lib/services/catia_provider.dart:3180-EOF",
              "result": "3180 | \n3181 |     final ctx = paletteContext;\n3182 |     final t = paletteTarget;\n3183 |     final visible = paletteVisibleEntries(_macros, ctx);\n3184 |     // TEŞHİS (seçim tipi filtresi sorunu): her push'ta bağlam + eşleşen\n3185 |     // öğe sayısı/adları log'a yazılır (fikatia_debug.log). Kalıcı — throttle\n3186 |     // yok, palet push'u zaten native JSON imzasıyla dedup edildiği için\n3187 |     // pratikte sık tetiklenmiyor.\n3188 |     debugPrint(\n3189 |       'PALET-PUSH: $ctx hedef=${t?.id ?? '-'} '\n3190 |       '-> ${visible.length}/${_macros.where((m) => m.inPalette).length} öğe '\n3191 |       '[${visible.map((m) => (m.label.isEmpty ? m.name : m.label) + (m.paletteSelectionTypes.isEmpty ? '' : ':${m.paletteSelectionTypes}')).join(', ')}]',\n3192 |     );\n3193 |     unawaited(\n3194 |       PaletteService.pushState(\n3195 |         PaletteStatePayload(\n3196 |           entries: [\n3197 |             for (final m in visible)\n3198 |               PaletteEntryView(\n3199 |                 id: m.id,\n3200 |                 emoji: m.emoji,\n3201 |                 label: m.label.isEmpty ? m.name : m.label,\n3202 |                 tooltip: m.isCommand\n3203 |                     ? 'Komut: ${m.command}'\n3204 |                     : (m.description.isEmpty ? m.name : m.description),\n3205 |                 enabled: t != null,\n3206 |               ),\n3207 |           ],\n3208 |           workbenchId: ctx.workbenchId,\n3209 |           docKind: ctx.docKind,\n3210 |           targetTitle: t?.title ?? '',\n3211 |           note: t == null ? 'bağlı CATIA yok' : '',\n3212 |           viewMode: _palette.viewMode,\n3213 |           showMode: _palette.showMode,\n3214 |           targetPinned: _paletteTargetPinned,\n3215 |           locked: _palette.locked,\n3216 |           hidden: _palette.hidden,\n3217 |           cards: _palette.cards,\n3218 |           // Kart listesi YALNIZ kart görünümü açıkken hesaplanır/kanaldan geçer\n3219 |           // (kapalıyken imza sade kalır, gereksiz trafik olmaz).\n3220 |           tiles: _palette.cards ? paletteTileViews : const <PaletteTileView>[],\n3221 |           // 🖥 Masaüstü listesi yalnız kart görünümü açıkken: kapalıyken hiçbir\n3222 |           // kartta 🖥 düğmesi yok, listeyi göndermek boş trafik olurdu.\n3223 |           desktops: _palette.cards\n3224 |               ? _paletteDesktops\n3225 |               : const <PaletteDesktopView>[],\n3226 |         ),\n3227 |       ),\n3228 |     );\n3229 |   }\n3230 | \n3231 |   // ---- 🗂 Palet KART görünümü (Aşama 1) ----\n3232 | \n3233 |   /// ✏️ Palet kartından gelen \"yeniden adlandır\" isteği. Palet penceresi\n3234 |   /// NOACTIVATE'dir → KLAVYE GİRİŞİ YOKTUR (palette_screen.dart I6 notu), bu\n3235 |   /// yüzden metin diyaloğu orada açılamaz: istek buraya gelir, popup öne\n3236 |   /// alınır ve ARAYÜZ diyaloğu açar (bkz. home_screen.dart `_TileRenameBridge`).\n3237 |   /// Arayüz diyaloğu kapattığında değer null'a çekilir.\n3238 |   final ValueNotifier<PaletteTileRenameRequest?> tileRenameRequest =\n3239 |       ValueNotifier(null);\n3240 | \n3241 |   /// 🖥✏️ Aynı düzende **masaüstü adı** isteği (Aşama 2). Palet NOACTIVATE →\n3242 |   /// alt pencerede metin alınamaz; diyalog ana pencerede açılır.\n3243 |   final ValueNotifier<PaletteDesktopRenameRequest?> tileDesktopRenameRequest =\n3244 |       ValueNotifier(null);\n3245 | \n3246 |   /// Palet kart görünümünün çizeceği kartlar — ana penceredeki liste ile AYNI\n3247 |   /// kaynak ([_cardInstances]) ve AYNI kurallar. Masaüstü etiketi, aktif\n3248 |   /// pencere vurgusu ve workbench rozeti burada çözülür; alt pencere registry/\n3249 |   /// DPI/tema mantığını hiç bilmez (tek karar kaynağı korunur).\n3250 |   ///\n3251 |   /// 📌 SIRALAMA: **hedef kart her zaman ilk sırada** (bkz. [_tileTargetFirst]).\n3252 |   ///\n3253 |   /// 🎯 YALNIZ HEDEF KART: palet pencerisi TEK kart çizer — **aktif (🎯 hedef)\n3254 |   /// CATIA'nın kartı** — ve o kart komut ızgarasının ÜSTÜNDE durur. Geri\n3255 |   /// kalan CATIA'lar listede BİLEREK gönderilmez: palet küçük bir yardımcı\n3256 |   /// pencere, odak her an başka bir CATIA'da olabilir. Başka kartı görmek/\n3257 |   /// yönetmek için ana pencere (ya da başlıktaki 🎯 ile hedef değiştirme).\n3258 |   ///\n3259 |   /// Hedef yoksa (palete bağlı CATIA yok) liste BOŞ gider → palet \"_Empty\"\n3260 |   /// yerine kart gövdesi boş kalır, kullanıcı yanlış kartta komut çalıştırmaz.\n3261 |   List<PaletteTileView> get paletteTileViews {\n3262 |     final target = paletteTarget;\n3263 |     if (target == null) return const [];\n3264 |     final i = target;\n3265 |     final effectivePid = realWindowPid(i.id) ?? i.pid;\n3266 |     return [\n3267 |       PaletteTileView(\n3268 |         id: i.id,\n3269 |         title: i.title,\n3270 |         caption: i.caption,\n3271 |         hasCustomTitle: i.hasCustomTitle,\n3272 |         fileName: i.fileName,\n3273 |         status: i.status,\n3274 |         desktopNumber: i.desktopNumber ?? 0,\n3275 |         desktopName: i.desktopName ?? '',\n3276 |         desktopLabel: desktopLabel(\n3277 |           i.desktopGuid,\n3278 |           desktopNumber: i.desktopNumber,\n3279 |         ),\n3280 |         workbenchId: i.workbenchId,\n3281 |         selectionCount: i.selectionCount,\n3282 |         pid: i.pid,\n3283 |         placeholder: i.placeholder,\n3284 |         openUnconnected: i.isOpenUnconnected,\n3285 |         connected: i.connected,\n3286 |         // 👻 kartın karşılığı bir pencere yok → hiç vurgulanmaz (ana karttaki\n3287 |         // kuralla aynı).\n3288 |         activeWindow:\n3289 |             !i.placeholder && effectivePid > 0 && effectivePid == _activePid,\n3290 |         scriptBusy: isScriptBusy(i.id),\n3291 |         progress: i.progress ?? -1,\n3292 |         // 🖥 Windows'un o an bulunulan masaüstü: CATIA'nınkiyle AYNI mi?\n3293 |         // Odak bilgisi (_activePid) BUNUNLA karıştırılmaz — odak, pencerenin\n3294 |         // nerede DEĞİL nerede olduğunu söylemez.\n3295 |         onCurrentDesktop: _isOnCurrentDesktop(i),\n3296 |         macros: _tileMacrosFor(i),\n3297 |         docs: [\n3298 |           for (final d in i.documents)\n3299 |             PaletteTileDocView(\n3300 |               id: d.id,\n3301 |               name: d.name,\n3302 |               saved: d.saved,\n3303 |               isActive: d.isActive,\n3304 |             ),\n3305 |         ],\n3306 |         closedDocs: [\n3307 |           for (final d in closedDocumentsFor(i.id))\n3308 |             PaletteTileDocView(id: d.name, name: d.name, closed: true),\n3309 |         ],\n3310 |       ),\n3311 |     ];\n3312 |   }\n3313 | \n3314 |   /// 🪟 Bu kartın pencereye gönderilecek TAM verisi (JSON).\n3315 |   ///\n3316 |   /// 📌 NEDEN `paletteTileViews` DEĞİL: o getter **hedef kartı** (🎯) döndürür\n3317 |   /// (palet tek kart çizer). Aktarılan kart hangisi olursa olsun **o** kartın\n3318 |   /// verisi gitmeli → model doğrudan bu instance için kurulur.\n3319 |   ///\n3320 |   /// Kart listeden KALDIRILIRSA (❌ / CATIA kapandı) null döner; çağıran taraf\n3321 |   /// pencereye \"kart yok\" bildirip pencereyi kapatır.\n3322 |   PaletteTileView? tileViewFor(String instanceId) {\n3323 |     final i = _cardInstances.where((x) => x.id == instanceId).firstOrNull;\n3324 |     if (i == null) return null;\n3325 |     final effectivePid = realWindowPid(i.id) ?? i.pid;\n3326 |     return PaletteTileView(\n3327 |       id: i.id,\n3328 |       title: i.title,\n3329 |       caption: i.caption,\n3330 |       hasCustomTitle: i.hasCustomTitle,\n3331 |       fileName: i.fileName,\n3332 |       status: i.status,\n3333 |       desktopNumber: i.desktopNumber ?? 0,\n3334 |       desktopName: i.desktopName ?? '',\n3335 |       desktopLabel: desktopLabel(i.desktopGuid, desktopNumber: i.desktopNumber),\n3336 |       workbenchId: i.workbenchId,\n3337 |       selectionCount: i.selectionCount,\n3338 |       pid: i.pid,\n3339 |       placeholder: i.placeholder,\n3340 |       openUnconnected: i.isOpenUnconnected,\n3341 |       connected: i.connected,\n3342 |       activeWindow:\n3343 |           !i.placeholder && effectivePid > 0 && effectivePid == _activePid,\n3344 |       scriptBusy: isScriptBusy(i.id),\n3345 |       progress: i.progress ?? -1,\n3346 |       onCurrentDesktop: _isOnCurrentDesktop(i),\n3347 |       macros: _tileMacrosFor(i),\n3348 |       docs: [\n3349 |         for (final d in i.documents)\n3350 |           PaletteTileDocView(\n3351 |             id: d.id,\n3352 |             name: d.name,\n3353 |             saved: d.saved,\n3354 |             isActive: d.isActive,\n3355 |           ),\n3356 |       ],\n3357 |       closedDocs: [\n3358 |         for (final d in closedDocumentsFor(i.id))\n3359 |           PaletteTileDocView(id: d.name, name: d.name, closed: true),\n3360 |       ],\n3361 |     );\n3362 |   }\n3363 | \n3364 |   /// 🪟 Kartı bağımsız pencereye AÇAR (kartın 🗖 düğmesi).\n3365 |   ///\n3366 |   /// Pencere \"tüm sanal masaüstlerinde\" çalışır, taşınabilir ve boyutlandırılabilir\n3367 |   /// (bkz. `CardWindowService.open` → `NativeBridge.setupCardWindow`).\n3368 |   /// Aynı kart için zaten pencere varsa yenisi açılmaz, o öne getirilir.\n3369 |   /// 🪟 Bu kartın aktarılmış penceresi şu an AÇIK mı? (düğmenin rengi bununla\n3370 |   /// değişir; kart widget'ı sadece BİLİR, karar vermez).\n3371 |   bool isCardWindowOpen(String id) => CardWindowService.isOpen(id);\n3372 | \n3373 |   /// 🧪 Yalnızca `--selftest-card` için: kart listesindeki N. kartın id'si.\n3374 |   ///\n3375 |   /// 📌 `openCardWindow` kartı `_cardInstances` içinde arıyor; selftest\n3376 |   ///   `instances` (filtrelenmiş liste) üzerinden seçince ikinci/üçüncü kart\n3377 |   ///   \"bulunamadı\" deyip 1 ms'de dönüyordu. Bu yardımcı aynı listeyi\n3378 |   ///   kullanır.\n3379 |   ///\n3380 |   /// 📌 `public` olmalı: `main.dart` BAŞKA dosyadan çağırıyor (Dart'ta\n3381 |   ///   alt çizgili üyeler dosya kapsamlıdır).\n3382 |   String? debugCardIdAt(int index) {\n3383 |     final list = _cardInstances;\n3384 |     if (index < 0 || index >= list.length) return null;\n3385 |     return list[index].id;\n3386 |   }\n3387 | \n3388 |   /// 🪟 Bu kartın aktarılmış penceresini açar.\n3389 |   Future<bool> openCardWindow(String instanceId) async {\n3390 |     final inst = _cardInstances.where((i) => i.id == instanceId).firstOrNull;\n3391 |     if (inst == null) {\n3392 |       _setSessionMessage('🪟 Kart bulunamadı', warn: true);\n3393 |       return false;\n3394 |     }\n3395 |     final ok = await CardWindowService.open(inst.id, inst.title);\n3396 |     if (!ok) {\n3397 |       _setSessionMessage('🪟 Pencere açılamadı', warn: true);\n3398 |       return false;\n3399 |     }\n3400 |     _setSessionMessage('🪟 \"${inst.title}\" ayrı pencereye aktarıldı');\n3401 |     // Canlı doku varsa durdur: pencereye kare PNG yoluyla akacak.\n3402 |     if (_textureIds.containsKey(instanceId)) {\n3403 |       await _stopLiveThumbTexture(instanceId);\n3404 |       _syncLiveThumbTimer();\n3405 |       unawaited(_captureLiveBatch());\n3406 |     }\n3407 |     // 🪟 YENİ PENCERE = yeni alıcı: o kareyi bu pencere bilmiyor, bu yüzden\n3408 |     //   görüntü ilk gönderimde ZORLA taşınmalı (aksi halde imza filtresi onu\n3409 |     //   eler ve pencere boş kalır).\n3410 |     _cardWindowThumbForce.add(instanceId);\n3411 |     // İlk veriyi hemen gönder (pencere hazır olmadan boş kalmasın).\n3412 |     // 📌 Servis `ready` kapısı varken bu çağrı veriyi `_lastState`'te\n3413 |     //   bekletir; `ready` gelince gönderilir.\n3414 |     unawaited(_pushCardWindowState(instanceId));\n3415 |     return true;\n3416 |   }\n3417 | \n3418 |   // -----------------------------------------------------------------------\n3419 |   // 🗂️ CANLI GÖRÜNÜM PENCERE YÖNETİCİSİ (minimal, ayrı pencere)\n3420 |   // -----------------------------------------------------------------------\n3421 |   //\n3422 |   // İSTEK (02.10.2026): kayıtlı (konumu hatırlanan) canlı görünüm pencereleri\n3423 |   // için ayrı bir yönetici; CATIA'sı bağlı olanları tek tek ya da toplu aç/kapat.\n3424 |   // Pencere arayüzü: lib/screens/window_manager_screen.dart. Tek yazıcı bu\n3425 |   // sınıftır; yönetici pencere yalnız istek yollar (bkz. WindowManagerService).\n3426 | \n3427 |   /// 🗂️ Pencere yöneticisini açar (Araçlar ▸ Pencere Yöneticisi).\n3428 |   Future<void> openWindowManager() async {\n3429 |     try {\n3430 |       await WindowManagerService.open(\n3431 |         entries: windowManagerEntries,\n3432 |         openOne: openRegisteredCardWindow,\n3433 |         closeOne: closeRegisteredCardWindow,\n3434 |         arrange: arrangeCardWindows,\n3435 |       );\n3436 |     } catch (e) {\n3437 |       _setSessionMessage('Pencere yöneticisi açılamadı: $e', warn: true);\n3438 |     }\n3439 |   }\n3440 | \n3441 |   /// 🧱 AÇIK canlı görünüm pencerelerini sağ üst köşeye küçük kart ızgarası\n3442 |   /// olarak DİZER (yönetici ▸ \"Diz\"). Boyut/konum hesabı saf fonksiyonda\n3443 |   /// ([tileGridTopRight]); burada yalnız ekran çalışma alanı okunur ve her\n3444 |   /// pencereye uygulanır. Uygulanan konum, konum defterine 3 sn içinde\n3445 |   /// kendiliğinden işlenir (pencere bir daha açılınca aynı yerde gelir).\n3446 |   /// Dönen sayı: dizilen pencere adedi.\n3447 |   Future<int> arrangeCardWindows(List<String> ids) async {\n3448 |     final open = [\n3449 |       for (final id in ids)\n3450 |         if (CardWindowService.isOpen(id)) id,\n3451 |     ];\n3452 |     if (open.isEmpty) return 0;\n3453 |     final display = await screenRetriever.getPrimaryDisplay();\n3454 |     final sf = (display.scaleFactor ?? 1.0).toDouble();\n3455 |     final size = display.visibleSize ?? display.size;\n3456 |     // screen_retriever mantıksal birim verir; pencereler FİZİKSEL px ister.\n3457 |     final work = WindowRect(\n3458 |       ((display.visiblePosition?.dx ?? 0) * sf).round(),\n3459 |       ((display.visiblePosition?.dy ?? 0) * sf).round(),\n3460 |       (size.width * sf).round(),\n3461 |       (size.height * sf).round(),\n3462 |     );\n3463 |     final rects = tileGridTopRight(count: open.length, work: work);\n3464 |     for (var i = 0; i < open.length; i++) {\n3465 |       await CardWindowService.applyRect(open[i], rects[i]);\n3466 |     }\n3467 |     return open.length;\n3468 |   }\n3469 | \n3470 |   /// 🗂️ Kayıtlı oturumdaki \"pencere kaydı olan\" CATIA'ları, o an bağlı\n3471 |   /// CATIA'larla EŞLER (TAŞIMASIZ, durum DEĞİŞTİRMEZ).\n3472 |   ///\n3473 |   /// Eşleşme: önce ortak açık döküman yolu (restoreSession'daki [_matchLive]\n3474 |   /// ile aynı ölçüt, 1'e 1 ve en çok ortak dökümana göre), sonra özel başlık\n3475 |   /// eşitliği. Eşleşmeyen kayıt `live: null` döner (CATIA bağlı değil).\n3476 |   List<({SavedInstance saved, CatiaInstance? live})> _wmSavedPairs() {\n3477 |     final session = _savedSession;\n3478 |     if (session == null) return const [];\n3479 |     final saved = <SavedInstance>[\n3480 |       for (final s in session.instances)\n3481 |         if (s.cardWindowOpen || s.cardWindowRect != null) s,\n3482 |     ];\n3483 |     if (saved.isEmpty) return const [];\n3484 |     final live = _liveInstances;\n3485 |     final livePaths = [\n3486 |       for (final l in live)\n3487 |         {\n3488 |           for (final d in l.documents)\n3489 |             if (d.path.isNotEmpty) '${d.path}${d.name}'.toLowerCase(),\n3490 |         },\n3491 |     ];\n3492 |     final pairs = <({int score, int li, int si})>[];\n3493 |     for (var li = 0; li < live.length; li++) {\n3494 |       for (var si = 0; si < saved.length; si++) {\n3495 |         final score = saved[si].documents\n3496 |             .where((d) => livePaths[li].contains(d.fullPath.toLowerCase()))\n3497 |             .length;\n3498 |         if (score > 0) pairs.add((score: score, li: li, si: si));\n3499 |       }\n3500 |     }\n3501 |     pairs.sort((a, b) => b.score.compareTo(a.score));\n3502 |     final liveOf = <int, int>{};\n3503 |     final usedLive = <int>{};\n3504 |     for (final p in pairs) {\n3505 |       if (liveOf.containsKey(p.si) || usedLive.contains(p.li)) continue;\n3506 |       liveOf[p.si] = p.li;\n3507 |       usedLive.add(p.li);\n3508 |     }\n3509 |     // İkinci tur: dökümanı değişmiş olsa bile ÖZEL adı aynı olan CATIA.\n3510 |     for (var si = 0; si < saved.length; si++) {\n3511 |       if (liveOf.containsKey(si)) continue;\n3512 |       final t = saved[si].title.trim().toLowerCase();\n3513 |       if (t.isEmpty || t == saved[si].id.trim().toLowerCase()) continue;\n3514 |       for (var li = 0; li < live.length; li++) {\n3515 |         if (usedLive.contains(li)) continue;\n3516 |         if (live[li].title.trim().toLowerCase() == t) {\n3517 |           liveOf[si] = li;\n3518 |           usedLive.add(li);\n3519 |           break;\n3520 |         }\n3521 |       }\n3522 |     }\n3523 |     return [\n3524 |       for (var si = 0; si < saved.length; si++)\n3525 |         (\n3526 |           saved: saved[si],\n3527 |           live: liveOf[si] == null ? null : live[liveOf[si]!],\n3528 |         ),\n3529 |     ];\n3530 |   }\n3531 | \n3532 |   /// 🗂️ Yönetici listesi: konumu kayıtlı her canlı görünüm penceresi.\n3533 |   ///\n3534 |   /// Kaynaklar (birleşik): kayıtlı oturum + kalıcı konum defteri + şu an açık\n3535 |   /// pencereler. CATIA'sı bağlı olanın `liveId`'si doludur.\n3536 |   Future<List<WmEntry>> windowManagerEntries() async {\n3537 |     await CardRectStore.ensureLoaded();\n3538 |     final out = <WmEntry>[];\n3539 |     final used = <String>{};\n3540 | \n3541 |     String sub(String desktop, String doc, WindowRect? r) => [\n3542 |       if (desktop.isNotEmpty && desktop != 'M?') desktop,\n3543 |       if (doc.isNotEmpty) doc,\n3544 |       if (r != null && r.isUsable) '${r.w}×${r.h}',\n3545 |     ].join(' · ');\n3546 | \n3547 |     for (final p in _wmSavedPairs()) {\n3548 |       final s = p.saved;\n3549 |       final l = p.live;\n3550 |       if (l != null) used.add(l.id);\n3551 |       final rect = l == null\n3552 |           ? s.cardWindowRect\n3553 |           : (CardWindowService.lastRectOf(l.id) ??\n3554 |                 CardRectStore.lookup(l.id, l.title) ??\n3555 |                 s.cardWindowRect);\n3556 |       final doc = l != null\n3557 |           ? (l.documents.where((d) => d.isActive).firstOrNull?.name ??\n3558 |                 l.fileName)\n3559 |           : (s.documents.where((d) => d.isActive).firstOrNull?.name ??\n3560 |                 s.documents.firstOrNull?.name ??\n3561 |                 '');\n3562 |       out.add(\n3563 |         WmEntry(\n3564 |           key: l?.id ?? 'saved:${s.id}',\n3565 |           title: l?.title ?? s.title,\n3566 |           subtitle: sub(\n3567 |             l != null && l.desktopNumber != null\n3568 |                 ? 'M${l.desktopNumber}'\n3569 |                 : s.desktopLabel,\n3570 |             doc,\n3571 |             rect,\n3572 |           ),\n3573 |           liveId: l?.id ?? '',\n3574 |           open: l != null && CardWindowService.isOpen(l.id),\n3575 |         ),\n3576 |       );\n3577 |     }\n3578 | \n3579 |     for (final l in _liveInstances) {\n3580 |       if (used.contains(l.id)) continue;\n3581 |       final rect =\n3582 |           CardWindowService.lastRectOf(l.id) ??\n3583 |           CardRectStore.lookup(l.id, l.title);\n3584 |       final isOpen = CardWindowService.isOpen(l.id);\n3585 |       if (rect == null && !isOpen) continue; // pencere kaydı yok\n3586 |       out.add(\n3587 |         WmEntry(\n3588 |           key: l.id,\n3589 |           title: l.title,\n3590 |           subtitle: sub(\n3591 |             l.desktopNumber != null ? 'M${l.desktopNumber}' : '',\n3592 |             l.documents.where((d) => d.isActive).firstOrNull?.name ??\n3593 |                 l.fileName,\n3594 |             rect,\n3595 |           ),\n3596 |           liveId: l.id,\n3597 |           open: isOpen,\n3598 |         ),\n3599 |       );\n3600 |     }\n3601 | \n3602 |     // Kararlı sıra: bağlı olanlar önce, sonra ada göre (satırlar zıplamasın).\n3603 |     out.sort((a, b) {\n3604 |       if (a.connected != b.connected) return a.connected ? -1 : 1;\n3605 |       return a.title.toLowerCase().compareTo(b.title.toLowerCase());\n3606 |     });\n3607 |     return out;\n3608 |   }\n3609 | \n3610 |   /// 🗂️ Bağlı CATIA'nın canlı görünüm penceresini KAYITLI konumda açar.\n3611 |   ///\n3612 |   /// Konum sırası ([CardWindowService.open] içinde): bu çalışmadaki son konum >\n3613 |   /// kalıcı defter. İkisi de yoksa ve kayıtlı oturumda konum varsa, o konum\n3614 |   /// önce hafızaya tohumlanır (daha yeni bir hafızayı EZMEZ).\n3615 |   /// CATIA bağlı değilse / pencere zaten açıksa false döner (iş yapılmaz).\n3616 |   Future<bool> openRegisteredCardWindow(String liveId) async {\n3617 |     final live = _liveInstances.where((i) => i.id == liveId).firstOrNull;\n3618 |     if (live == null) return false;\n3619 |     if (CardWindowService.isOpen(live.id)) return true;\n3620 |     try {\n3621 |       await CardRectStore.ensureLoaded();\n3622 |       if (CardWindowService.lastRectOf(live.id) == null &&\n3623 |           CardRectStore.lookup(live.id, live.title) == null) {\n3624 |         final saved = _wmSavedPairs()\n3625 |             .where((p) => p.live?.id == live.id)\n3626 |             .firstOrNull\n3627 |             ?.saved;\n3628 |         CardWindowService.rememberRect(live.id, saved?.cardWindowRect);\n3629 |       }\n3630 |     } catch (e) {\n3631 |       debugPrint('pencere yöneticisi: konum tohumlanamadı ($e)');\n3632 |     }\n3633 |     return openCardWindow(live.id);\n3634 |   }\n3635 | \n3636 |   /// 🗂️ Bağlı CATIA'nın canlı görünüm penceresini kapatır (kart listede KALIR).\n3637 |   Future<bool> closeRegisteredCardWindow(String liveId) async {\n3638 |     if (!CardWindowService.isOpen(liveId)) return false;\n3639 |     await CardWindowService.close(liveId);\n3640 |     return true;\n3641 |   }\n3642 | \n3643 |   /// 📸 Bu kartın EKRAN GÖRÜNTÜSÜ (base64 PNG) — pencere kart gibi görünsün.\n3644 |   ///\n3645 |   /// 📌 TV kartı listede bir EKRAN GÖRÜNTÜSÜ gösterir; pencere de aynı görünmeli.\n3646 |   ///   Görüntü kanal ile gider: alt pencere kendi `CatiaProvider`'ı olmadığı için\n3647 |   ///   `thumbFor`'a erişemez (bkz. `palette_tile.dart` baş notu).\n3648 |   ///\n3649 |   /// 📌 BOYUT: PNG base64 ~1.3 kat büyür. Bu yüzden görüntü **her turda değil,\n3650 |   ///   sadece değiştiğinde** gönderilir (`CardWindowService` imza karşılaştırması).\n3651 |   String thumbBase64(String instanceId) {\n3652 |     final png = thumbFor(instanceId);\n3653 |     if (png == null || png.isEmpty) return '';\n3654 |     return base64Encode(png);\n3655 |   }\n3656 | \n3657 |   /// 🪟 Penceredeki eylemler: bu pencere kendi sahte `_instances`'ıyla çalışır\n3658 |   /// (veri ana pencereden gelir), ama eylem **gerçek** provider'da yürütülmelidir\n3659 |   /// — çünkü `focusInstance`/`saveDocument` gibi işler `_ghosts`, gerçek pid\n3660 |   /// eşlemesi ve COM bağlantısı gerektirir; sahte provider'da bunlar boştur\n3661 |   /// ve düğmeler \"hiçbir şey olmadı\" gibi görünürdü.\n3662 |   ///\n3663 |   /// 📌 Bu yüzden kart penceresindeki TÜM düğmeler kanal üzerinden ana\n3664 |   ///   pencereye gider, orada `handlePaletteTileAction` (listenin ve paletin\n3665 |   ///   kullandığı AYNI kapı) yürütür → \"aynı fonksiyonlar\" birebir sağlanır.\n3666 |   Future<void> actionFocus(String id) async {\n3667 |     _sendToMainWindow(\n3668 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.focus),\n3669 |     );\n3670 |   }\n3671 | \n3672 |   /// 🪟 Başlık değişikliği isteğini ana pencereye gönderir.\n3673 |   ///\n3674 |   /// 📌 [newTitle] kart penceresinde `EditableTitle` ile YAZILAN metindir.\n3675 |   /// Doldurulursa ana pencere diyalog açmadan doğrudan uygular (pencere\n3676 |   /// odak alabiliyor + yazılabiliyor → diyaloğa gerek yok). Boş bırakılırsa\n3677 |   /// ana pencereyi öne alıp yeniden adlandırma diyaloğu açılır.\n3678 |   Future<void> actionRename(String id, [String newTitle = '']) async {\n3679 |     _sendToMainWindow(\n3680 |       PaletteTileRequest(\n3681 |         instanceId: id,\n3682 |         action: PaletteTileAction.rename,\n3683 |         newTitle: newTitle,\n3684 |       ),\n3685 |     );\n3686 |   }\n3687 | \n3688 |   Future<void> actionRestart(String id) async {\n3689 |     _sendToMainWindow(\n3690 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.restart),\n3691 |     );\n3692 |   }\n3693 | \n3694 |   Future<void> actionQuit(String id) async {\n3695 |     _sendToMainWindow(\n3696 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.quit),\n3697 |     );\n3698 |   }\n3699 | \n3700 |   Future<void> actionRemove(String id) async {\n3701 |     _sendToMainWindow(\n3702 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.remove),\n3703 |     );\n3704 |   }\n3705 | \n3706 |   Future<void> actionSaveDoc(String id, String docId) async {\n3707 |     _sendToMainWindow(\n3708 |       PaletteTileRequest(\n3709 |         instanceId: id,\n3710 |         action: PaletteTileAction.saveDoc,\n3711 |         docId: docId,\n3712 |       ),\n3713 |     );\n3714 |   }\n3715 | \n3716 |   Future<void> actionActivateDoc(String id, String docId) async {\n3717 |     _sendToMainWindow(\n3718 |       PaletteTileRequest(\n3719 |         instanceId: id,\n3720 |         action: PaletteTileAction.activateDoc,\n3721 |         docId: docId,\n3722 |       ),\n3723 |     );\n3724 |   }\n3725 | \n3726 |   Future<void> actionOpenDocFolder(String id, String docId) async {\n3727 |     _sendToMainWindow(\n3728 |       PaletteTileRequest(\n3729 |         instanceId: id,\n3730 |         action: PaletteTileAction.openDocFolder,\n3731 |         docId: docId,\n3732 |       ),\n3733 |     );\n3734 |   }\n3735 | \n3736 |   Future<void> actionCloseDoc(String id, String docId) async {\n3737 |     _sendToMainWindow(\n3738 |       PaletteTileRequest(\n3739 |         instanceId: id,\n3740 |         action: PaletteTileAction.closeDoc,\n3741 |         docId: docId,\n3742 |       ),\n3743 |     );\n3744 |   }\n3745 | \n3746 |   Future<void> actionMoveToDesktop(String id, int index, String name) async {\n3747 |     _sendToMainWindow(\n3748 |       PaletteTileRequest(\n3749 |         instanceId: id,\n3750 |         action: PaletteTileAction.moveToDesktop,\n3751 |         desktopIndex: index,\n3752 |         desktopName: name,\n3753 |       ),\n3754 |     );\n3755 |   }\n3756 | \n3757 |   Future<void> actionRunMacro(String id, String macroId) async {\n3758 |     _sendToMainWindow(\n3759 |       PaletteTileRequest(\n3760 |         instanceId: id,\n3761 |         action: PaletteTileAction.runMacro,\n3762 |         macroId: macroId,\n3763 |       ),\n3764 |     );\n3765 |   }\n3766 | \n3767 |   // ---- Kart penceresinde ana pencereye yönlendirilen EK eylemler ----\n3768 |   Future<void> actionToggleLive(String id) async {\n3769 |     _sendToMainWindow(\n3770 |       PaletteTileRequest(instanceId: id, action: PaletteTileAction.toggleLive),\n3771 |     );\n3772 |   }\n3773 | \n3774 |   Future<void> actionReopenClosedDoc(\n3775 |     String id,\n3776 |     String name,\n3777 |     String path,\n3778 |   ) async {\n3779 |     _sendToMainWindow(\n3780 |       PaletteTileRequest(\n3781 |         instanceId: id,\n3782 |         action: PaletteTileAction.reopenClosedDoc,\n3783 |         docName: name,\n3784 |         docPath: path,\n3785 |       ),\n3786 |     );\n3787 |   }\n3788 | \n3789 |   Future<void> actionForgetClosedDoc(\n3790 |     String id,\n3791 |     String name,\n3792 |     String path,\n3793 |   ) async {\n3794 |     _sendToMainWindow(\n3795 |       PaletteTileRequest(\n3796 |         instanceId: id,\n3797 |         action: PaletteTileAction.forgetClosedDoc,\n3798 |         docName: name,\n3799 |         docPath: path,\n3800 |       ),\n3801 |     );\n3802 |   }\n3803 | \n3804 |   Future<void> actionRenameDesktop(String id) async {\n3805 |     _sendToMainWindow(\n3806 |       PaletteTileRequest(\n3807 |         instanceId: id,\n3808 |         action: PaletteTileAction.renameDesktop,\n3809 |       ),\n3810 |     );\n3811 |   }\n3812 | \n3813 |   Future<void> actionSwitchDesktop(String id, String name) async {\n3814 |     _sendToMainWindow(\n3815 |       PaletteTileRequest(\n3816 |         instanceId: id,\n3817 |         action: PaletteTileAction.switchToItsDesktop,\n3818 |         desktopName: name,\n3819 |       ),\n3820 |     );\n3821 |   }\n3822 | \n3823 |   Future<void> actionMoveNamed(String id, String name) async {\n3824 |     _sendToMainWindow(\n3825 |       PaletteTileRequest(\n3826 |         instanceId: id,\n3827 |         action: PaletteTileAction.moveToDesktop,\n3828 |         desktopName: name,\n3829 |       ),\n3830 |     );\n3831 |   }\n3832 | \n3833 |   /// 🪟 PENCERE MODU: ana pencereden gelen JSON ile TEK kartı yükler.\n3834 |   ///\n3835 |   /// 📌 AMAÇ: Alt pencere gerçek `CatiaListTile` widget'ını çizsin. O widget\n3836 |   ///   `CatiaInstance` bekler; bu metot kanal JSON'unu (`PaletteTileView`\n3837 |   ///   alanları) tam bir `CatiaInstance`'a çevirir. Böylece pencere, listedeki\n3838 |   ///   kartla **aynı veriyle** çizilir — kopya bir kart değil.\n3839 |   ///\n3840 |   /// 📌 `thumb` (base64 PNG) ayrıca `_thumbs`'a yazılır: `CatiaListTile` görüntüyü\n3841 |   ///   `thumbFor(id)` ile okur (bkz. `catia_list_tile.dart` `_buildThumb`).\n3842 |   // PENCLER KOPRUSU: eylem istegini ANA PENCEREYE gonder.\n3843 |   //\n3844 |   // NEDEN `CardWindowService.onTileAction` DEGIL: o geri cagirma yalniz\n3845 |   // ANA pencerede baglanir (constructor). Alt pencerede NULL'dur ve\n3846 |   // `?.call` sessizce hicbir sey yapmaz -> \"dikmeler calismiyor\".\n3847 |   void _sendToMainWindow(PaletteTileRequest req) {\n3848 |     if (_cardWindowMode) {\n3849 |       unawaited(CardWindowService.sendTileFromChild(req));\n3850 |     }\n3851 |   }\n3852 | \n3853 |   void applyCardWindowState(Map<String, dynamic> json, String thumbB64) {\n3854 |     if (!_cardWindowMode) return;\n3855 |     final id = (json['id'] is String) ? json['id'] as String : '';\n3856 |     if (_instances.isEmpty) {\n3857 |       debugPrint(\n3858 |         'CARD-APPLY: ilk state id=\"$id\" gone=${json['gone']} '\n3859 |         'anahtarlar=${json.keys.length}',\n3860 |       );\n3861 |     }\n3862 |     if (id.isEmpty) return;\n3863 | \n3864 |     // 🛡️ PENCERE MODU SAVUNMASI: kart listeden kalkarsa (❌ düğmesi, CATIA\n3865 |     //   kapandı) ana pencere `null` gönderir. O durumda pencereyi kapatmadan\n3866 |     //   önce KART ARTIK YOKTUR — ama liste boşalınca ekran \"veri bekleniyor\"\n3867 |     //   ekranına düşer ve kullanıcı kartın **listedeki kopyasına geri\n3868 |     //   döndüğünü** sanır. Bu yüzden burada kart yoksa `_instances` BOŞ\n3869 |     //   bırakılır ve ana pencere pencereyi kapatır.\n3870 |     if (json['gone'] == true) {\n3871 |       _instances = <CatiaInstance>[];\n3872 |       notifyListeners();\n3873 |       return;\n3874 |     }\n3875 | \n3876 |     CatiaStatus status;\n3877 |     try {\n3878 |       status = CatiaStatus.values.byName(\n3879 |         (json['status'] is String) ? json['status'] as String : 'idle',\n3880 |       );\n3881 |     } catch (_) {\n3882 |       status = CatiaStatus.idle;\n3883 |     }\n3884 |     int intOr(String k, [int d = 0]) =>\n3885 |         (json[k] is num) ? (json[k] as num).toInt() : d;\n3886 | \n3887 |     // 📄 Döküman listesi (kanaldan gelen JSON'dan).\n3888 |     final docs = <CatiaDocument>[];\n3889 |     final rawDocs = json['docs'];\n3890 |     if (rawDocs is List) {\n3891 |       for (final d in rawDocs) {\n3892 |         if (d is! Map) continue;\n3893 |         final name = (d['name'] is String) ? d['name'] as String : '';\n3894 |         if (name.isEmpty) continue;\n3895 |         docs.add(\n3896 |           CatiaDocument(\n3897 |             id: (d['id'] is String) ? d['id'] as String : name,\n3898 |             name: name,\n3899 |             path: '',\n3900 |             saved: d['saved'] != false,\n3901 |             isActive: d['isActive'] == true,\n3902 |           ),\n3903 |         );\n3904 |       }\n3905 |     }\n3906 | \n3907 |     final inst = CatiaInstance(\n3908 |       id: id,\n3909 |       fileName: (json['fileName'] is String) ? json['fileName'] as String : '',\n3910 |       status: status,\n3911 |       progress: intOr('progress', -1) >= 0 ? intOr('progress', -1) : null,\n3912 |       uptime: Duration.zero,\n3913 |       pid: intOr('pid'),\n3914 |       worker: 'kart penceresi',\n3915 |       connected: json['connected'] == true,\n3916 |       lastError: null,\n3917 |       caption: (json['caption'] is String) ? json['caption'] as String : '',\n3918 |       desktopGuid: (json['desktopGuid'] is String)\n3919 |           ? json['desktopGuid'] as String\n3920 |           : '',\n3921 |     );\n3922 | \n3923 |     // 📌 `title`/`caption` `copyWith`'te DEĞİL (constructor alanları) → bu yüzden\n3924 |     //   başlık `displayName` üzerinden verilir; `CatiaListTile` başlığı oradan\n3925 |     //   okur. `caption` alanı doğrudan kurucuya aynı değerle verilmiştir.\n3926 |     _instances = [\n3927 |       inst.copyWith(\n3928 |         displayName: (json['title'] is String) ? json['title'] as String : id,\n3929 |         desktopName: (json['desktopName'] is String)\n3930 |             ? json['desktopName'] as String\n3931 |             : null,\n3932 |         desktopNumber: intOr('desktopNumber', 0) > 0\n3933 |             ? intOr('desktopNumber', 0)\n3934 |             : null,\n3935 |         workbenchId: (json['workbenchId'] is String)\n3936 |             ? json['workbenchId'] as String\n3937 |             : '',\n3938 |         selectionCount: intOr('selectionCount'),\n3939 |         selectionTypes: const <String>{},\n3940 |         computing: json['computing'] == true,\n3941 |         documents: docs,\n3942 |       ),\n3943 |     ];\n3944 |     _mockActive = false;\n3945 | \n3946 |     // 🪟 CANLI VERİ (Görev 5): ana pencerenin gönderdiği ek alanlar. Bunlar\n3947 |     // yazılmazsa alt pencerede ▶/🖥/🕘/👻/🔴 düğmeleri boş ya da ölü kalır.\n3948 |     final placeholder = json['placeholder'] == true;\n3949 |     final openUnc = json['openUnconnected'] == true;\n3950 |     final pidNow = intOr('pid');\n3951 |     _activePid = json['activeWindow'] == true\n3952 |         ? (pidNow > 0 ? pidNow : 1)\n3953 |         : null;\n3954 |     if (placeholder || openUnc) {\n3955 |       _instances = [_instances.first.copyWith()]\n3956 |           .map(\n3957 |             (e) => CatiaInstance(\n3958 |               id: e.id,\n3959 |               fileName: e.fileName,\n3960 |               status: e.status,\n3961 |               pid: e.pid,\n3962 |               worker: e.worker,\n3963 |               connected: e.connected,\n3964 |               displayName: e.displayName,\n3965 |               progress: e.progress,\n3966 |               documents: e.documents,\n3967 |               desktopGuid: e.desktopGuid,\n3968 |               desktopNumber: e.desktopNumber,\n3969 |               desktopName: e.desktopName,\n3970 |               caption: e.caption,\n3971 |               workbenchId: e.workbenchId,\n3972 |               selectionCount: e.selectionCount,\n3973 |               computing: e.computing,\n3974 |               placeholder: true,\n3975 |               openPid: openUnc ? 1 : 0,\n3976 |             ),\n3977 |           )\n3978 |           .toList();\n3979 |     }\n3980 |     // ⏳/📊 percent (ilerleme) — `progress` yoksa `percent` kullanılır.\n3981 |     final pct = intOr('percent', -1);\n3982 |     if (pct >= 0 &&\n3983 |         _instances.isNotEmpty &&\n3984 |         _instances.first.progress == null) {\n3985 |       _instances = [_instances.first.copyWith(progress: pct)];\n3986 |     }\n3987 |     // 🔴 canlı görüntü bayrakları\n3988 |     _remoteLive = json['live'] == true;\n3989 |     _remoteLiveOk = json['liveOk'] != false;\n3990 |     // ▶ Makro listesi (ana pencerenin bağlama uyan kayıtları) + meşgul bayrağı\n3991 |     final rawMacros = json['macros'];\n3992 |     _macros = <SavedMacro>[\n3993 |       if (rawMacros is List)\n3994 |         for (final m in rawMacros)\n3995 |           if (m is Map && m['id'] is String && m['label'] is String)\n3996 |             SavedMacro(\n3997 |               id: m['id'] as String,\n3998 |               name: m['label'] as String,\n3999 |               label: m['label'] as String,\n4000 |               path: '',\n4001 |               emoji: (m['emoji'] is String) ? m['emoji'] as String : '',\n4002 |               description: (m['tooltip'] is String)\n4003 |                   ? m['tooltip'] as String\n4004 |                   : '',\n4005 |               // `code`/`command` dolu ⇒ `isCode/isCommand` true ⇒ çalıştırılabilir\n4006 |               // görünür; gerçek yürütme ANA pencerede id ile yapılır.\n4007 |               command: m['isCommand'] == true ? (m['label'] as String) : null,\n4008 |               code: m['isCommand'] == true ? null : '',\n4009 |             ),\n4010 |     ];\n4011 |     _scriptBusy\n4012 |       ..clear()\n4013 |       ..addAll(json['scriptBusy'] == true ? <String>{id} : <String>{});\n4014 |     // 🕘 Geçmiş dökümanlar\n4015 |     final rawClosed = json['closedDocs'];\n4016 |     _closedDocs[id] = [\n4017 |       if (rawClosed is List)\n4018 |         for (final d in rawClosed)\n4019 |           if (d is Map && d['name'] is String)\n4020 |             ClosedCatiaDocument(\n4021 |               name: d['name'] as String,\n4022 |               path: '',\n4023 |               closedAt: DateTime.now(),\n4024 |             ),\n4025 |     ];\n4026 |     // 💬 SORULAR: ana pencereden gelen CATIA diyalogları.\n4027 |     //\n4028 |     // 🐛 29.09.2026 — KULLANICI: \"dışarı sabitlenen pencerede tile card'taki\n4029 |     //   gibi dialog göstermiyor.\" NEDEN: gönderici (`_pushCardWindowState`)\n4030 |     //   `dialogs` alanını taşımıyor, burada da `_dialogsById` doldurulmuyordu.\n4031 |     //   Alt pencerenin kendi `CatiaProvider`'ı bu yüzden `dialogsOf(id)` →\n4032 |     //   boş liste, `hasDialog(id)` → false döndürüyordu; ❓ rozeti çıkmadığı için\n4033 |     //   `_buildDialogs` erkenden `SizedBox.shrink()` dönüyor, panel hiç açılmıyordu.\n4034 |     //   Kart penceresi gerçek `CatiaListTile` çizdiği için veri şarttı.\n4035 |     //\n4036 |     // 📌 DİYALOG BOŞSA KÜME TEMİZLENİR: soru kapandığında eski kayıt kalırsa\n4037 |     //   pencere \"cevaplanmış ama hâlâ görünen\" bir soruyu gösterirdi.\n4038 |     final dialogs = _dialogsFrom(json['dialogs']);\n4039 |     if (dialogs.isEmpty) {\n4040 |       _dialogsById.remove(id);\n4041 |     } else {\n4042 |       _dialogsById[id] = dialogs;\n4043 |     }\n4044 |     // 🖥 Masaüstü listesi + motor\n4045 |     final rawDesk = json['desktops'];\n4046 |     if (rawDesk is List) {\n4047 |       _desktopList = [\n4048 |         for (final d in rawDesk)\n4049 |           if (d is Map)\n4050 |             DesktopInfo(\n4051 |               index: (d['index'] is num) ? (d['index'] as num).toInt() : -1,\n4052 |               name: (d['name'] is String) ? d['name'] as String : '',\n4053 |               visible: d['visible'] == true,\n4054 |               guid: (d['guid'] is String) ? d['guid'] as String : '',\n4055 |             ),\n4056 |       ];\n4057 |     }\n4058 |     final eng = json['desktopEngine'];\n4059 |     if (eng is String) {\n4060 |       _settings = _settings.copyWith(\n4061 |         desktopEngine: eng == 'powershell'\n4062 |             ? DesktopEngine.powershell\n4063 |             : DesktopEngine.native,\n4064 |       );\n4065 |     }\n4066 | \n4067 |     // 📸 Görüntü: kartın ekran görüntüsü (`CatiaListTile` `thumbFor` ile okur).\n4068 |     //\n4069 |     // 🔴 GÖRÜNTÜ DE AYNI İÇERİKTEYSE ELENİR. `base64Decode` her seferinde\n4070 |     //   YENİ bir `Uint8List` üretir; karşılaştırma `identical()` ile yapılsa\n4071 |     //   \"her turda değişmiş\" sayılır ve `_thumbs[id]` her defasında yeni\n4072 |     //   nesneyle değişir → `CatiaListTile` kareyi YENİDEN decode eder,\n4073 |     //   kart tam yeniden çizilir. Bu, komut çalışırken penceredeki titreme\n4074 |     //   ve kasmanın kaynağıydı. Artık İÇERİK hash'i karşılaştırılır.\n4075 |     var thumbChanged = false;\n4076 |     if (thumbB64.isNotEmpty) {\n4077 |       try {\n4078 |         final bytes = base64Decode(thumbB64);\n4079 |         final h = _fingerprintBytes(bytes);\n4080 |         thumbChanged = h != _cardWindowThumbHash[id];\n4081 |         _cardWindowThumbHash[id] = h;\n4082 |         // STAB-9: doğrudan `_thumbs[id] = bytes` 24'lük sınırı ve `_thumbOrder`\n4083 |         // sırasını atlıyordu (uzun çalışmada kart penceresi görüntüleri birikir).\n4084 |         _storeThumb(id, bytes);\n4085 |       } catch (_) {\n4086 |         // Bozuk base64 → görsel yok, kart yine çizilir.\n4087 |         thumbChanged = true;\n4088 |       }\n4089 |     }\n4090 |     // 🔴 ALT PENCEREDE GEREKSİZ ÇİZİM KESİCİ.\n4091 |     //\n4092 |     //   `notifyListeners()` → `CardWindowScreen` `context.watch` ile dinliyor →\n4093 |     //   kartın TÜM ağacı (gradient + blur + gölge + döküman listesi) baştan\n4094 |     //   çiziliyor ve yeni PNG için `_decodeThumb` tetikleniyor.\n4095 |     //\n4096 |     //   Ana pencere artık imza filtresiyle aynı veriyi göndermiyor; yine de\n4097 |     //   savunma olarak burada da \"görüntü yoksa ve metin imzası aynıysa\"\n4098 |     //   çizim yapılmaz.\n4099 |     final sig = _cardWindowSigOf(json);\n4100 |     if (!thumbChanged && sig == _cardWindowStateSig[id]) return;\n4101 |     _cardWindowStateSig[id] = sig;\n4102 |     notifyListeners();\n4103 |   }\n4104 | \n4105 |   /// 🪟 Pencereyi AÇ (görsel test + teşhis). `openCardWindow`'a ek olarak\n4106 |   /// ayrıntılı log yazar: HWND bulundu mu, setupCardWindow başarılı mı,\n4107 |   /// kanal açıldı mı. Kullanıcı \"pencere açılmıyor\" derse bu log tek\n4108 |   /// bakışta nerede koptuğunu söyler.\n4109 |   Future<bool> openCardWindowVerbose(String instanceId) async {\n4110 |     final ok = await openCardWindow(instanceId);\n4111 |     _dlog(\n4112 |       'PENCERE: $instanceId istenildi -> sonuc=$ok '\n4113 |       'kayitli=${CardWindowService.isOpen(instanceId)} '\n4114 |       'pencereSayisi=${CardWindowService.openIds.length}',\n4115 |     );\n4116 |     return ok;\n4117 |   }\n4118 | \n4119 |   /// 🪟 instanceId -> pencereye gönderilmiş SON ham PNG nesnesi.\n4120 |   ///\n4121 |   /// 📌 Amaç: `thumbBase64` çağrısını gereksiz yere yapmamak. `Uint8List`\n4122 |   ///   her yakalamada YENİ bir nesne olduğu için nesne kimliği\n4123 |   ///   (`identical`) güvenilir bir \"kare değişti\" sinyalidir. Aynı nesne ise\n4124 |   ///   kare değişmemiştir → 226 KB'lık PNG encode'u hiç yapılmaz.\n4125 |   final Map<String, Uint8List> _cardWindowThumbRefs = <String, Uint8List>{};\n4126 | \n4127 |   /// 🪟 Bir sonraki gönderimde görüntüyü `thumb` alanıyla ZORLA gönder.\n4128 |   /// (Pencere yeni açıldığında eski imza kaydı hâlâ duruyor olabilir.)\n4129 |   final Set<String> _cardWindowThumbForce = <String>{};\n4130 | \n4131 |   /// 🪟 instanceId -> pencereye gönderilmiş SON durum imzası.\n4132 |   ///\n4133 |   /// 🔴 PERFORMANS (kasma düzeltmesi): `pushState` her çağrıda kanala\n4134 |   /// yazıyordu; oysa komut/makro çalışırken bile kartın gösterilebilir\n4135 |   /// verisi çoğu turda BİREBİR AYNI'dır (durum, başlık, rozet, ilerleme\n4136 |   /// değişmemiştir). Her turda N pencereye tam JSON yazmak:\n4137 |   ///   • ana pencere UI thread'ini `invokeMethod` beklerken kilitler,\n4138 |   ///   • alt pencerede `notifyListeners()` → kartın TAMAMEN yeniden\n4139 |   ///     çizilmesi + PNG decode tetikler,\n4140 |   /// yani \"pencere sayısı × snapshot sıklığı\" ile katlanan bir yüktür.\n4141 |   ///\n4142 |   /// ✅ ÇÖZÜM: gösterilecek alanların kısa bir imzası tutulur; imza\n4143 |   /// değişmediyse gönderim TAMAMEN atlanır (görüntü değiştiyse o da\n4144 |   /// imzaya katılır, yoksa sessizce eskir).\n4145 |   final Map<String, String> _cardWindowStateSig = <String, String>{};\n4146 | \n4147 |   /// 🪟 PNG baytlarının içerik parmak izi (hızlı, çakışma düşük).\n4148 |   ///\n4149 |   /// 📌 NEDEN İÇERİK HASH'İ: `identical()` yalnız NESNE KİMLİĞİ yalar.\n4150 |   /// `_storeThumb` her yakalamada yeni `Uint8List` ürettiği için aynı\n4151 |   /// görüntü bile \"değişmiş\" sayılır → her turda 226 KB base64 encode +\n4152 |   /// kanal trafiği. Bu hash ile AYNI İÇERİK elenir; sadece gerçekten yeni\n4153 |   /// kare gider. (64-bit FNV-1a; 226 KB'ın tamamı taranır, saniyede\n4154 |   /// birkaç kez çalışır — decode/JSON'un yanında ihmal edilebilir.)\n4155 |   static int _fingerprintBytes(Uint8List bytes) {\n4156 |     var h = 0xcbf29ce484222325;\n4157 |     for (var i = 0; i < bytes.length; i++) {\n4158 |       h ^= bytes[i];\n4159 |       h *= 0x100000001b3;\n4160 |     }\n4161 |     // Uzunluk da karışır (aynı hash, farklı boyut olasılığını düşürür).\n4162 |     h ^= bytes.length;\n4163 |     return h & 0x7fffffffffffffff;\n4164 |   }\n4165 | \n4166 |   /// 🪟 instanceId -> son gönderilen görüntünün içerik parmak izi.\n4167 |   final Map<String, int> _cardWindowThumbHash = <String, int>{};\n4168 | \n4169 |   /// 🪟 Kart penceresine gönderilecek durumun kısa imzası.\n4170 |   ///\n4171 |   /// `thumb` BİLEREK dışarıda bırakıldı: görüntü zaten kendi hash'iyle\n4172 |   /// denetleniyor ve 226 KB'lık bir alanı imzalamak pahalı olurdu.\n4173 |   static String _cardWindowSigOf(Map<String, dynamic> payload) {\n4174 |     final keys = payload.keys.toList()..sort();\n4175 |     final sb = StringBuffer();\n4176 |     for (final k in keys) {\n4177 |       if (k == 'thumb') continue;\n4178 |       final v = payload[k];\n4179 |       // Liste/harita değişken uzunlukta olabilir → kimliği değil, kısa\n4180 |       // özet (`jsonEncode`) kullanılır. Büyük listeler (desktops/macros)\n4181 |       // JSON'a çevrildiği için bu da tam maliyet değildir; asıl kazanç\n4182 |       // \"hiç göndermemek\"ten gelir.\n4183 |       sb\n4184 |         ..write(k)\n4185 |         ..write('=')\n4186 |         ..write(v is List || v is Map ? jsonEncode(v).hashCode : '$v')\n4187 |         ..write(';');\n4188 |     }\n4189 |     return sb.toString();\n4190 |   }\n4191 | \n4192 |   // 🖼️ Kartın penceresi açıkken doku kullanılabilir mi? (SEÇENEK B)\n4193 |   //\n4194 |   // 📌 `live` kipinde HAYIR: alt pencere canlı kareyi PNG olarak bekler ve doku\n4195 |   //   yalnızca ana engine'de gösterilebildiği için kart listede doku alamaz.\n4196 |   // 🧊 `frozen` kipinde EVET: pencere son kareyi dondurur, o kart listede doku\n4197 |   //   alır ve `_captureLiveBatch` onu PNG turundan ATLAR (doku varsa `continue`).\n4198 |   //   Böylece o kart için PrintWindow + encode + base64 + kanal + decode\n4199 |   //   zincirinin tamamı devre dışı kalır.\n4200 |   bool get _cardFrameFrozen => _settings.cardFrameMode == CardFrameMode.frozen;\n4201 | \n4202 |   /// ⏱️ SEÇENEK C: kart → son PNG gönderim zamanı (ms). Hız sınırını uygular.\n4203 |   final Map<String, int> _cardWindowThumbSentAt = <String, int>{};\n4204 | \n4205 |   /// 📤 Aktarılmış kartın güncel verisini pencerelerine gönderir.\n4206 |   ///\n4207 |   /// Her provider değişikliğinde (snapshot/poller) çağrılır. AÇIK PENCERESİ\n4208 |   /// olmayan kartlar için native'e hiç gidilmez (`isOpen` kontrolü) — boşuna\n4209 |   /// kanal trafiği oluşturmamak için.\n4210 |   Future<void> _pushCardWindowState(String instanceId) async {\n4211 |     if (!CardWindowService.isOpen(instanceId)) return;\n4212 |     final view = tileViewFor(instanceId);\n4213 |     if (view == null) {\n4214 |       // 📌 KART GERÇEKTEN YOK (kullanıcı ❌'e bastı / CATIA kapandı): pencere\n4215 |       //   kapatılır ve ALTA `gone` bildirimi gönderilir.\n4216 |       //\n4217 |       //   🐛 ÖNCEKİ HATA (\"bir süre sonra listeye geri dönüyor, bir daha dışarı\n4218 |       //   çıkmıyor\"): kart GEÇİCİ olarak `_cardInstances` içinde bulunamıyordu\n4219 |       //   (ghost taraması/filtre tam o anda boş döndü) ve biz pencereyi\n4220 |       //   kapatıyorduk. Kullanıcı kartın listedeki kopyasına dönüyor, pencere\n4221 |       //   kapanmış oluyordu.\n4222 |       //\n4223 |       //   ÇÖZÜM: kart yoksa HEMEN kapatma. Önce \"kart yok\" sinyali gönder;\n4224 |       //   pencere boş kalır. Kart bir sonraki snapshot'ta geri gelirse pencere\n4225 |       //   kendiliğinden dolar. Yalnızca kart GERÇEKTEN kalktıysa (aşağıdaki\n4226 |       //   kontrol) pencere kapatılır.\n4227 |       final stillKnown =\n4228 |           _instances.any((i) => i.id == instanceId) ||\n4229 |           _ghosts.any((g) => g.id == instanceId);\n4230 |       if (stillKnown) {\n4231 |         // Kart listede ama görünüm/filtre dışında: pencereye boş gönder,\n4232 |         // kapanırma YOK.\n4233 |         await CardWindowService.pushState(instanceId, {'gone': true});\n4234 |         return;\n4235 |       }\n\n[Showing lines 3180-4235 of 10452. Use start_line/end_line to read other sections.]",
              "success": true
            }
          ]
        }
      ],
      "ts": 1791343460487
    },
    {
      "id": "ad871339-4d50-4eb4-ac7c-1c35c54479d6",
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "<user_input mode=\"act\">hepsini hallet</user_input>"
        }
      ],
      "ts": 1791343475408
    }
  ]
}
