From 147c651dc17a7b0ed9d911f66070e0bba7af44a4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 15:58:04 +0800 Subject: [PATCH 001/166] Write the run window's own notices in the IDE language --- CLAUDE.md | 1 + README.md | 2 +- README/README_zh-CN.md | 2 + README/README_zh-TW.md | 2 + architecture_explore.md | 9 ++-- docs/updates/2026-09-b.md | 9 ++++ docs/updates/README.md | 3 +- .../process_executor/file_runner_process.py | 17 +++---- .../process_executor_utils.py | 7 +-- .../python_task_process_manager.py | 5 +- .../extend/process_executor/run_notice.py | 27 +++++++++++ .../extend_multi_language/extend_english.py | 14 ++++++ .../extend_traditional_chinese.py | 14 ++++++ test/test_utils/test_run_notice.py | 46 +++++++++++++++++++ 14 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 pybreeze/extend/process_executor/run_notice.py create mode 100644 test/test_utils/test_run_notice.py diff --git a/CLAUDE.md b/CLAUDE.md index 0fdfe2f2..3161388d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,7 @@ pybreeze/ │ │ ├── process_executor_utils.py # build_process / start_process / run_dir_files_* │ │ ├── file_runner_process.py # FileRunnerProcess — plugin run configs (any language) │ │ ├── queue_pump.py # Shared pipe reader + per-tick queue drain +│ │ ├── run_notice.py # run_notice: a run window's own [Error]/[Run]/… lines, translated │ │ ├── api_testka/ auto_control/ web_runner/ load_density/ │ │ ├── file_automation/ mail_thunder/ # Each delegates to build_process with its package name │ │ ├── test_pioneer/ # python -m test_pioneer -e via start_module_process diff --git a/README.md b/README.md index 6bee0d70..311637c8 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Both dictionaries carry the same 708 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 721 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index b45e056a..e93fa6c5 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -113,6 +113,8 @@ IDE 界面支持多种语言: - **繁体中文** - 可通过插件添加其他语言 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言显示。 + --- ## 架构设计 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 08a57468..76acc8f0 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -113,6 +113,8 @@ IDE 介面支援多種語言: - **繁體中文** - 可透過插件新增其他語言 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。 + --- ## 架構設計 diff --git a/architecture_explore.md b/architecture_explore.md index 858bd70d..0f0c7ff6 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -50,7 +50,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ┌─────────────────────────────────────────────────────────────────────────┐ │ 基礎層 Foundation │ │ pybreeze/utils/ 18 個工具子套件(純邏輯,可單測) │ - │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 708 鍵) │ + │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 721 鍵) │ └─────────────────────────────────────────────────────────────────────────┘ ▼ 外部子行程:python -m je_api_testka / je_auto_control / je_web_runner / @@ -66,7 +66,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 708 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 721 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示 @@ -158,6 +158,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `pump_message_queue(q, append_fn, is_error, max_messages)` — UI 執行緒用。`MAX_MESSAGES_PER_PUMP = 256`:每 tick 只抽一則的話輸出上限只有 ~10 行/秒,聒噪的腳本會爬行;有上界則避免洪水輸出卡住 UI 執行緒。`max_messages=None` 是收尾時一次抽乾。只跳過空字串 - `output_queue()` — 每條管線的 queue 最多 `MAX_QUEUED_MESSAGES`(10,000)則;滿了 reader 就等(每 0.2 秒看一次 `keep_reading`),子行程寫管線也跟著等,跑得跟視窗顯示一樣快,像終端機;以前不設上限,印個不停的腳本會一直吃記憶體,按 Stop 後再一口氣全倒進視窗 - `ReaderGrace` / `any_alive()` / `OUTPUT_STILL_HELD_NOTE` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 +- 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …` 等 13 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 - 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 - `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`_insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 @@ -362,7 +363,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 708 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 721 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -455,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 124 個 `test_*.py`、2251 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2255 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 66e6d9f7..f277daff 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -640,3 +640,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: `test/test_utils/test_regex_tester.py` passes on the 3.10 matrix leg; normal patterns still compile, escaped `\(` and `[(]` are not counted. - **Files**: `pybreeze/utils/regex_tools/regex_tester.py` - **Evidence**: crash trace `sre_parse.py _parse_sub/_parse` on Python 3.10.11; ruff clean. + +## U-20260924-271 · 2026-09-24 · A run window's own notices in the IDE language · #fix #i18n #run-window + +- **What**: what the executors write into a run window about the run itself was English whatever the IDE spoke: `[Error] Command not found: gcc`, `[Compile] ...`, `[Compile failed] exit code 1`, `[Run] ...`, `[Stopped]`, `[Error] Timed out after 120s`, `[Error] No Python interpreter found: ...`, `[Error] APITestka runs the script in the editor tab in front; ...`, `[Mail] The test report was not sent: ...` and four more, thirteen f-string literals in `file_runner_process.py`, `process_executor_utils.py` and `python_task_process_manager.py`. +- **Fix**: `extend/process_executor/run_notice.py`: `run_notice(notice, /, **fields)` takes `run_window_` from the language dictionary (13 entries in each language; the English reads as before) and fills in the fields; the notice name is positional-only, so `[Run] {name}` can have a field called `name`. When the IDE's dictionary lacks the entry (an executor started before `update_language_dict()`, as in a script or a test) PyBreeze's English is used. +- **Tests**: `test_run_notice.py` (4): a notice in Traditional Chinese; English when the dictionary lacks it; every notice the executors name is in both dictionaries; no executor writes an `"[Error] ..."`-style literal. The run-window, compile-then-run and mail-notice tests pass unchanged. +- **Result / numbers**: 721 keys in each language. 2255 tests in 125 files. `ruff check` clean. +- **Files**: `pybreeze/extend/process_executor/run_notice.py` (new), `pybreeze/extend/process_executor/file_runner_process.py`, `pybreeze/extend/process_executor/process_executor_utils.py`, `pybreeze/extend/process_executor/python_task_process_manager.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_run_notice.py` (new), `CLAUDE.md`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: the reason a report mail was not sent comes from `mail_thunder_extend` and is still English. diff --git a/docs/updates/README.md b/docs/updates/README.md index cf018570..e199c9a4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-271 | 2026-09-24 | A run window's own notices in the IDE language | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | | U-20260924-270 | 2026-09-24 | Report a deeply nested regex instead of crashing on CPython 3.10 | #fix #regex | [2026-09-b](2026-09-b.md) | | U-20260924-269 | 2026-09-24 | Adopt je_editor 1.0.27 docked-editor contract | #done #cross-project | [2026-09-b](2026-09-b.md) | | U-20260924-268 | 2026-09-24 | A file that is not a diagram, and a refused CoT URL, say why in the IDE language | #fix #i18n #diagram #ai | [2026-09-b](2026-09-b.md) | @@ -352,4 +353,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 68 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 69 | diff --git a/pybreeze/extend/process_executor/file_runner_process.py b/pybreeze/extend/process_executor/file_runner_process.py index 3a8a6352..78e92d42 100644 --- a/pybreeze/extend/process_executor/file_runner_process.py +++ b/pybreeze/extend/process_executor/file_runner_process.py @@ -28,6 +28,7 @@ read_stream_into_queue, ) from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow +from pybreeze.extend.process_executor.run_notice import run_notice from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.utils.subprocess_util import ( no_window_creationflags, own_session_options, stop_tree, utf8_subprocess_env, @@ -101,7 +102,7 @@ def run_file(self, run_config: dict, file_path: str) -> None: compiler = run_config.get("compiler") if not isinstance(compiler, str) or not compiler: # A plugin's config is not checked by JEditor when it registers. - self.main_window.append_output("[Error] The run config names no compiler\n", is_error=True) + self.main_window.append_output(run_notice("no_compiler"), is_error=True) return args = run_arguments(run_config) @@ -128,20 +129,20 @@ def _compile_and_run(self, compiler: str, args: list, output_flag: str, file_pat output_name += ".exe" compile_cmd = [compiler] + args + [file_path, output_flag, output_name] - self.main_window.append_output(f"[Compile] {' '.join(compile_cmd)}\n", is_error=False) + self.main_window.append_output(run_notice("compile", command=' '.join(compile_cmd)), is_error=False) def run_if_compiled(exit_code: int) -> None: if self._cancelled: # Stopped: while compiling (reported as a failed compile), or # just after it succeeded (the binary ran anyway) - self.main_window.append_output("[Stopped]\n", is_error=True, own_line=True) + self.main_window.append_output(run_notice("stopped"), is_error=True, own_line=True) self._remove_build_dir(build_dir) return if exit_code != 0: - self.main_window.append_output(f"[Compile failed] exit code {exit_code}\n", is_error=True) + self.main_window.append_output(run_notice("compile_failed", code=exit_code), is_error=True) self._remove_build_dir(build_dir) return - self.main_window.append_output(f"[Run] {output_name}\n", is_error=False) + self.main_window.append_output(run_notice("run", name=output_name), is_error=False) self._start_process([output_name], cleanup_dir=build_dir) self._start_process( @@ -179,7 +180,7 @@ def _start_process(self, command: list[str], cleanup_dir: str | None = None, **own_session_options(), ) except FileNotFoundError: - self.main_window.append_output(f"[Error] Command not found: {command[0]}\n", is_error=True) + self.main_window.append_output(run_notice("command_not_found", command=command[0]), is_error=True) self._remove_build_dir(cleanup_dir) return except OSError as error: @@ -187,7 +188,7 @@ def _start_process(self, command: list[str], cleanup_dir: str | None = None, # by antivirus: this raised out of the menu, or out of the timer # slot after a compile, and the window said nothing. self.main_window.append_output( - f"[Error] Could not start {command[0]}: {error.strerror or error}\n", is_error=True) + run_notice("could_not_start", command=command[0], reason=error.strerror or error), is_error=True) self._remove_build_dir(cleanup_dir) return @@ -242,7 +243,7 @@ def _pull_text(self) -> None: elif self._deadline is not None and time.monotonic() > self._deadline: self._deadline = None self.main_window.append_output( - f"[Error] Timed out after {COMPILE_TIME_LIMIT_SECONDS}s\n", is_error=True) + run_notice("timed_out", seconds=COMPILE_TIME_LIMIT_SECONDS), is_error=True) stop_tree(self.process) def _finish(self) -> None: diff --git a/pybreeze/extend/process_executor/process_executor_utils.py b/pybreeze/extend/process_executor/process_executor_utils.py index 47bb4a5a..362b7c40 100644 --- a/pybreeze/extend/process_executor/process_executor_utils.py +++ b/pybreeze/extend/process_executor/process_executor_utils.py @@ -14,6 +14,7 @@ from pybreeze.extend.mail_thunder_extend.mail_thunder_setting import DEFAULT_REPORT_PATH, send_after_test from pybreeze.extend.process_executor.python_task_process_manager import TaskProcessManager from pybreeze.utils.file_process.get_dir_file_list import get_dir_files_as_list +from pybreeze.extend.process_executor.run_notice import run_notice from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.pybreeze_ui.plain_text import as_text @@ -54,7 +55,7 @@ def report_no_script_tab( pybreeze_logger.error("%s run needs an editor tab in front", package) process = build_task_process(main_window, program_buffer=program_buffer) process.main_window.append_output( - f"[Error] {package} runs the script in the editor tab in front; open it and try again\n", + run_notice("needs_editor_tab", package=package), is_error=True, own_line=True) process.main_window.show() @@ -204,9 +205,9 @@ class _MailNotice(QObject): def tell(self, reason: str | None) -> None: """Called on the mail thread with ``send_report``'s answer.""" if reason is None: - self.told.emit("[Mail] The test report was sent\n", False) + self.told.emit(run_notice("mail_sent"), False) else: - self.told.emit(f"[Mail] The test report was not sent: {reason}\n", True) + self.told.emit(run_notice("mail_not_sent", reason=reason), True) def report_mail_hook(code_window: CodeWindow) -> Callable[[], None]: diff --git a/pybreeze/extend/process_executor/python_task_process_manager.py b/pybreeze/extend/process_executor/python_task_process_manager.py index 2963c665..6c124eca 100644 --- a/pybreeze/extend/process_executor/python_task_process_manager.py +++ b/pybreeze/extend/process_executor/python_task_process_manager.py @@ -25,6 +25,7 @@ read_stream_into_queue, ) from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow +from pybreeze.extend.process_executor.run_notice import run_notice from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.utils.subprocess_util import ( no_window_creationflags, own_session_options, stop_tree, utf8_subprocess_env, @@ -110,7 +111,7 @@ def renew_path(self) -> bool: except JEditorExecException as error: pybreeze_logger.error("No Python interpreter found for run: %r", error) self.main_window.append_output( - f"[Error] No Python interpreter found: {error}\n", is_error=True, own_line=True) + run_notice("no_interpreter", error=error), is_error=True, own_line=True) self.main_window.show() return False else: @@ -211,7 +212,7 @@ def _spawn_and_pump( pybreeze_logger.error("%s could not start: %r", package, error) self._remove_script_file() self.main_window.append_output( - f"[Error] {package} could not start: {error.strerror or error}\n", + run_notice("package_could_not_start", package=package, reason=error.strerror or error), is_error=True, own_line=True) self.main_window.show() return diff --git a/pybreeze/extend/process_executor/run_notice.py b/pybreeze/extend/process_executor/run_notice.py new file mode 100644 index 00000000..a14c2aff --- /dev/null +++ b/pybreeze/extend/process_executor/run_notice.py @@ -0,0 +1,27 @@ +"""What the executors write into a run window about the run itself, in the IDE language. + +A compile, a run starting, a stop, a command that is not there: each is a line +of its own in the window, ``[Error] ...`` or ``[Run] ...``, and they were all +English whatever the IDE spoke. +""" +from __future__ import annotations + +from je_editor import language_wrapper + +from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict + +# The language-dictionary key of a notice is this followed by its name +RUN_NOTICE_KEY_PREFIX = "run_window_" + + +def run_notice(notice: str, /, **fields: object) -> str: + """The notice ``run_window_`` with *fields* filled in, as one line (newline included). + + *notice* is positional-only, so a field may be called anything (``[Run] {name}``). + PyBreeze's English is used when the IDE's dictionary lacks the notice: + an executor started before ``update_language_dict()`` (a script, a test) + still says what happened. + """ + key = RUN_NOTICE_KEY_PREFIX + notice + template = language_wrapper.language_word_dict.get(key) or pybreeze_english_word_dict[key] + return template.format(**fields) + "\n" diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 9aa4194a..bae1a13c 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -771,6 +771,20 @@ "plugin_browser_status_downloading": "Downloading {name}...", "plugin_browser_status_installed": "Installed: {path}", "plugin_browser_restart_hint": "Plugin downloaded to:\n{path}\n\nPlease restart the editor to activate.", + # A run window's own notices about the run (extend/process_executor/run_notice.py) + "run_window_no_compiler": "[Error] The run config names no compiler", + "run_window_compile": "[Compile] {command}", + "run_window_stopped": "[Stopped]", + "run_window_compile_failed": "[Compile failed] exit code {code}", + "run_window_run": "[Run] {name}", + "run_window_command_not_found": "[Error] Command not found: {command}", + "run_window_could_not_start": "[Error] Could not start {command}: {reason}", + "run_window_timed_out": "[Error] Timed out after {seconds}s", + "run_window_needs_editor_tab": "[Error] {package} runs the script in the editor tab in front; open it and try again", + "run_window_mail_sent": "[Mail] The test report was sent", + "run_window_mail_not_sent": "[Mail] The test report was not sent: {reason}", + "run_window_no_interpreter": "[Error] No Python interpreter found: {error}", + "run_window_package_could_not_start": "[Error] {package} could not start: {reason}", } # Why a tool refused its input (pybreeze_ui/error_text.py): in English, the # constants the tools raise diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index d4a96197..1c1d1a8a 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -827,6 +827,20 @@ "error_text_server_error_error": "伺服器錯誤:{body}", "error_text_diagram_not_an_object_error": "架構圖檔案應該是一個物件,而不是 {kind}", "error_text_diagram_section_not_a_list_error": "架構圖的 '{section}' 應該是清單,而不是 {kind}", + # 執行視窗自己對這次執行的說明(extend/process_executor/run_notice.py) + "run_window_no_compiler": "[錯誤] 這個執行設定沒有指定編譯器", + "run_window_compile": "[編譯] {command}", + "run_window_stopped": "[已停止]", + "run_window_compile_failed": "[編譯失敗] 結束代碼 {code}", + "run_window_run": "[執行] {name}", + "run_window_command_not_found": "[錯誤] 找不到指令:{command}", + "run_window_could_not_start": "[錯誤] 無法啟動 {command}:{reason}", + "run_window_timed_out": "[錯誤] 超過 {seconds} 秒仍未完成", + "run_window_needs_editor_tab": "[錯誤] {package} 執行的是最前面那個編輯分頁裡的腳本;請先開啟腳本再試一次", + "run_window_mail_sent": "[郵件] 已寄出測試報告", + "run_window_mail_not_sent": "[郵件] 沒有寄出測試報告:{reason}", + "run_window_no_interpreter": "[錯誤] 找不到 Python 直譯器:{error}", + "run_window_package_could_not_start": "[錯誤] 無法啟動 {package}:{reason}", } diff --git a/test/test_utils/test_run_notice.py b/test/test_utils/test_run_notice.py new file mode 100644 index 00000000..7cb8b78f --- /dev/null +++ b/test/test_utils/test_run_notice.py @@ -0,0 +1,46 @@ +"""A run window's own notices ([Error], [Run], [Mail] ...) in the IDE language.""" +from __future__ import annotations + +import pathlib +import re + +import pybreeze +from pybreeze.extend.process_executor import run_notice as run_notice_mod +from pybreeze.extend.process_executor.run_notice import RUN_NOTICE_KEY_PREFIX, run_notice +from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict as ENGLISH +from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict as CHINESE, +) + + +def test_a_notice_in_chinese(monkeypatch): + # "[Error] Command not found: gcc" whatever the IDE spoke + monkeypatch.setattr(run_notice_mod.language_wrapper, "language_word_dict", CHINESE) + + assert run_notice("command_not_found", command="gcc") == "[錯誤] 找不到指令:gcc\n" + assert run_notice("run", name="build/main.exe") == "[執行] build/main.exe\n" + + +def test_english_when_the_ide_dictionary_lacks_the_notice(monkeypatch): + # An executor started before update_language_dict() (a script, a test) + monkeypatch.setattr(run_notice_mod.language_wrapper, "language_word_dict", {}) + + assert run_notice("compile_failed", code=3) == "[Compile failed] exit code 3\n" + + +def test_every_notice_the_executors_use_is_in_both_dictionaries(): + executors = pathlib.Path(pybreeze.__file__).parent / "extend" / "process_executor" + used = {name for path in executors.rglob("*.py") + for name in re.findall(r'run_notice\(\s*"(\w+)"', path.read_text(encoding="utf-8"))} + + assert used + assert not {RUN_NOTICE_KEY_PREFIX + name for name in used} - set(ENGLISH) + assert not {RUN_NOTICE_KEY_PREFIX + name for name in used} - set(CHINESE) + + +def test_no_executor_writes_an_english_notice_of_its_own(): + # They were f"[Error] ..." literals in append_output and the mail notice + executors = pathlib.Path(pybreeze.__file__).parent / "extend" / "process_executor" + literal = re.compile(r'f?"\[(?:Error|Run|Compile|Compile failed|Stopped|Mail)\]') + offenders = [path.name for path in executors.rglob("*.py") if literal.search(path.read_text(encoding="utf-8"))] + assert not offenders From e8bfb155f6b6bb5c853a7f8028fc574ac9df788a Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 16:02:09 +0800 Subject: [PATCH 002/166] Refactor: return the report mail's reasons from exception_tags --- README.md | 2 +- architecture_explore.md | 8 +++--- docs/updates/2026-09-b.md | 9 ++++++ docs/updates/README.md | 3 +- .../mail_thunder_setting.py | 28 +++++++++++++------ .../extend_traditional_chinese.py | 8 ++++++ pybreeze/utils/exception/exception_tags.py | 10 +++++++ 7 files changed, 53 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 311637c8..3b4d4319 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 721 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 729 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/architecture_explore.md b/architecture_explore.md index 0f0c7ff6..feb81b9d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -50,7 +50,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ┌─────────────────────────────────────────────────────────────────────────┐ │ 基礎層 Foundation │ │ pybreeze/utils/ 18 個工具子套件(純邏輯,可單測) │ - │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 721 鍵) │ + │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 729 鍵) │ └─────────────────────────────────────────────────────────────────────────┘ ▼ 外部子行程:python -m je_api_testka / je_auto_control / je_web_runner / @@ -66,7 +66,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 721 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 729 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示 @@ -363,7 +363,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 721 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 729 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2255 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2263 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index f277daff..03be4831 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -649,3 +649,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 721 keys in each language. 2255 tests in 125 files. `ruff check` clean. - **Files**: `pybreeze/extend/process_executor/run_notice.py` (new), `pybreeze/extend/process_executor/file_runner_process.py`, `pybreeze/extend/process_executor/process_executor_utils.py`, `pybreeze/extend/process_executor/python_task_process_manager.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_run_notice.py` (new), `CLAUDE.md`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: the reason a report mail was not sent comes from `mail_thunder_extend` and is still English. + +## U-20260924-272 · 2026-09-24 · Refactor: the reasons a report mail was not sent come from exception_tags · #refactor #i18n #mail + +- **What**: `send_after_test` answers why a run's report was not mailed with eight English literals (`no mail user is set`, `the mail server login failed`, `sending failed (SMTPException)`, `the run wrote no default_name.html`, ...), which the run window shows after `[Mail] The test report was not sent:` (U-20260924-271). They are `mail_*_error` and `report_*_error` constants in `exception_tags` now, with the same text. +- **Behaviour**: none visible; the Traditional Chinese entries are in the dictionary for the next step. +- **Tests**: `test_error_text.py` finds each of the eight again from a message made from it (+8); the mail-notice tests pass unchanged. +- **Result / numbers**: 729 keys in each language. 2263 tests in 125 files. `ruff check` clean. +- **Files**: `pybreeze/utils/exception/exception_tags.py`, `pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `README.md`, `architecture_explore.md` +- **Open items**: showing them in the IDE language (next entry). diff --git a/docs/updates/README.md b/docs/updates/README.md index e199c9a4..3dd2d773 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-272 | 2026-09-24 | Refactor: the reasons a report mail was not sent come from exception_tags | #refactor #i18n #mail | [2026-09-b](2026-09-b.md) | | U-20260924-271 | 2026-09-24 | A run window's own notices in the IDE language | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | | U-20260924-270 | 2026-09-24 | Report a deeply nested regex instead of crashing on CPython 3.10 | #fix #regex | [2026-09-b](2026-09-b.md) | | U-20260924-269 | 2026-09-24 | Adopt je_editor 1.0.27 docked-editor contract | #done #cross-project | [2026-09-b](2026-09-b.md) | @@ -353,4 +354,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 69 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 70 | diff --git a/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py b/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py index 36d1c4ec..f814b37b 100644 --- a/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py +++ b/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py @@ -4,7 +4,17 @@ import threading from collections.abc import Callable -from pybreeze.utils.exception.exception_tags import send_html_exception_tag +from pybreeze.utils.exception.exception_tags import ( + mail_login_failed_error, + mail_no_user_error, + mail_not_installed_error, + mail_send_failed_error, + mail_settings_unreadable_error, + report_missing_error, + report_not_a_file_error, + report_stale_error, + send_html_exception_tag, +) from pybreeze.utils.exception.exceptions import ITESendHtmlReportException from pybreeze.utils.logging.logger import pybreeze_logger @@ -64,7 +74,7 @@ def send_report(html_report_path: str | None = None, *, not_before: float | None from je_mail_thunder.utils.exception.exceptions import MailThunderException except ImportError as error: pybreeze_logger.error("Cannot send the report without je_mail_thunder: %r", error) - return "je_mail_thunder is not installed" + return mail_not_installed_error report_path = html_report_path if html_report_path is not None else DEFAULT_REPORT_PATH problem = _report_problem(report_path, not_before) @@ -78,10 +88,10 @@ def send_report(html_report_path: str | None = None, *, not_before: float | None # system, raised out of the mail thread except (OSError, ValueError, MailThunderException) as error: pybreeze_logger.error("The mail settings file could not be read: %r", error) - return "the mail settings file (mail_thunder_content.json) could not be read" + return mail_settings_unreadable_error if user is None: pybreeze_logger.error("Cannot determine mail user for sending report") - return "no mail user is set" + return mail_no_user_error try: with open(report_path, encoding="utf-8") as file: html_string = file.read() @@ -98,12 +108,12 @@ def send_report(html_report_path: str | None = None, *, not_before: float | None mail_thunder_smtp.send_message(message) except ITESendHtmlReportException as error: pybreeze_logger.error("%r %s", error, send_html_exception_tag) - return "the mail server login failed" + return mail_login_failed_error # OSError covers the socket and every smtplib error; ValueError a report # that is not UTF-8 except (OSError, ValueError, MailThunderException) as error: pybreeze_logger.error("Failed to send report: %r", error) - return f"sending failed ({type(error).__name__})" + return mail_send_failed_error.format(kind=type(error).__name__) return None @@ -113,11 +123,11 @@ def _report_problem(report_path: str, not_before: float | None) -> str | None: try: written = os.stat(report_path) except OSError: - return f"the run wrote no {name}" + return report_missing_error.format(name=name) if not os.path.isfile(report_path): - return f"{name} is not a file" + return report_not_a_file_error.format(name=name) if not_before is not None and written.st_mtime < not_before - _MTIME_SLACK_SECONDS: - return f"the run wrote no new {name}; the one there is from an earlier run" + return report_stale_error.format(name=name) return None diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 1c1d1a8a..e859f1a3 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -827,6 +827,14 @@ "error_text_server_error_error": "伺服器錯誤:{body}", "error_text_diagram_not_an_object_error": "架構圖檔案應該是一個物件,而不是 {kind}", "error_text_diagram_section_not_a_list_error": "架構圖的 '{section}' 應該是清單,而不是 {kind}", + "error_text_mail_not_installed_error": "沒有安裝 je_mail_thunder", + "error_text_mail_settings_unreadable_error": "無法讀取郵件設定檔(mail_thunder_content.json)", + "error_text_mail_no_user_error": "沒有設定郵件使用者", + "error_text_mail_login_failed_error": "郵件伺服器登入失敗", + "error_text_mail_send_failed_error": "寄送失敗({kind})", + "error_text_report_missing_error": "這次執行沒有寫出 {name}", + "error_text_report_not_a_file_error": "{name} 不是檔案", + "error_text_report_stale_error": "這次執行沒有寫出新的 {name};現有的是之前執行留下的", # 執行視窗自己對這次執行的說明(extend/process_executor/run_notice.py) "run_window_no_compiler": "[錯誤] 這個執行設定沒有指定編譯器", "run_window_compile": "[編譯] {command}", diff --git a/pybreeze/utils/exception/exception_tags.py b/pybreeze/utils/exception/exception_tags.py index 7779d2f8..690016fa 100644 --- a/pybreeze/utils/exception/exception_tags.py +++ b/pybreeze/utils/exception/exception_tags.py @@ -104,3 +104,13 @@ # A diagram file (.diagram.json) that is not one diagram_not_an_object_error: str = "a diagram file holds an object, not a {kind}" diagram_section_not_a_list_error: str = "a diagram's '{section}' is a list, not a {kind}" + +# Why a run's report mail was not sent (mail_thunder_extend.send_after_test) +mail_not_installed_error: str = "je_mail_thunder is not installed" +mail_settings_unreadable_error: str = "the mail settings file (mail_thunder_content.json) could not be read" +mail_no_user_error: str = "no mail user is set" +mail_login_failed_error: str = "the mail server login failed" +mail_send_failed_error: str = "sending failed ({kind})" +report_missing_error: str = "the run wrote no {name}" +report_not_a_file_error: str = "{name} is not a file" +report_stale_error: str = "the run wrote no new {name}; the one there is from an earlier run" From 5607e56d6d1f002bb7287369905cbea8ea4a34e8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 16:05:46 +0800 Subject: [PATCH 003/166] Say why a report mail was not sent in the IDE language --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 ++++++++ docs/updates/README.md | 3 ++- .../process_executor_utils.py | 3 ++- test/test_utils/test_run_notice.py | 21 +++++++++++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index feb81b9d..02fafdf4 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2263 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2264 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 03be4831..b7049c7e 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -658,3 +658,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 729 keys in each language. 2263 tests in 125 files. `ruff check` clean. - **Files**: `pybreeze/utils/exception/exception_tags.py`, `pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `README.md`, `architecture_explore.md` - **Open items**: showing them in the IDE language (next entry). + +## U-20260924-273 · 2026-09-24 · Why a report mail was not sent, in the IDE language · #fix #i18n #mail + +- **What**: after U-20260924-271 the run window's mail notice was translated, but the reason after it was not: `[郵件] 沒有寄出測試報告:no mail user is set`. +- **Fix**: `_MailNotice.tell` passes `send_after_test`'s answer through `error_text()`, which finds it among the constants U-20260924-272 made; a reason from anywhere else is shown as it is. +- **Tests**: `test_run_notice.py` +1 (no mail user, in Traditional Chinese: `[郵件] 沒有寄出測試報告:沒有設定郵件使用者`); the English mail-notice tests pass unchanged. +- **Result / numbers**: 2264 tests in 125 files. `ruff check` clean. +- **Files**: `pybreeze/extend/process_executor/process_executor_utils.py`, `test/test_utils/test_run_notice.py`, `architecture_explore.md` §18 +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 3dd2d773..a845c4f6 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-273 | 2026-09-24 | Why a report mail was not sent, in the IDE language | #fix #i18n #mail | [2026-09-b](2026-09-b.md) | | U-20260924-272 | 2026-09-24 | Refactor: the reasons a report mail was not sent come from exception_tags | #refactor #i18n #mail | [2026-09-b](2026-09-b.md) | | U-20260924-271 | 2026-09-24 | A run window's own notices in the IDE language | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | | U-20260924-270 | 2026-09-24 | Report a deeply nested regex instead of crashing on CPython 3.10 | #fix #regex | [2026-09-b](2026-09-b.md) | @@ -354,4 +355,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 70 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 71 | diff --git a/pybreeze/extend/process_executor/process_executor_utils.py b/pybreeze/extend/process_executor/process_executor_utils.py index 362b7c40..f6e638f0 100644 --- a/pybreeze/extend/process_executor/process_executor_utils.py +++ b/pybreeze/extend/process_executor/process_executor_utils.py @@ -16,6 +16,7 @@ from pybreeze.utils.file_process.get_dir_file_list import get_dir_files_as_list from pybreeze.extend.process_executor.run_notice import run_notice from pybreeze.utils.logging.logger import pybreeze_logger +from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.pybreeze_ui.plain_text import as_text if TYPE_CHECKING: @@ -207,7 +208,7 @@ def tell(self, reason: str | None) -> None: if reason is None: self.told.emit(run_notice("mail_sent"), False) else: - self.told.emit(run_notice("mail_not_sent", reason=reason), True) + self.told.emit(run_notice("mail_not_sent", reason=error_text(reason)), True) def report_mail_hook(code_window: CodeWindow) -> Callable[[], None]: diff --git a/test/test_utils/test_run_notice.py b/test/test_utils/test_run_notice.py index 7cb8b78f..8b021017 100644 --- a/test/test_utils/test_run_notice.py +++ b/test/test_utils/test_run_notice.py @@ -44,3 +44,24 @@ def test_no_executor_writes_an_english_notice_of_its_own(): literal = re.compile(r'f?"\[(?:Error|Run|Compile|Compile failed|Stopped|Mail)\]') offenders = [path.name for path in executors.rglob("*.py") if literal.search(path.read_text(encoding="utf-8"))] assert not offenders + + +def test_why_a_report_mail_was_not_sent_in_chinese(monkeypatch): + # "[郵件] 沒有寄出測試報告:no mail user is set" + import os + + os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + from PySide6.QtWidgets import QApplication + + from pybreeze.extend.process_executor.process_executor_utils import _MailNotice + from pybreeze.utils.exception.exception_tags import mail_no_user_error + + QApplication.instance() or QApplication([]) + monkeypatch.setattr(run_notice_mod.language_wrapper, "language_word_dict", CHINESE) + notice = _MailNotice() + told: list = [] + notice.told.connect(lambda text, is_error: told.append((text, is_error))) + + notice.tell(mail_no_user_error) + + assert told == [("[郵件] 沒有寄出測試報告:沒有設定郵件使用者\n", True)] From d895eefdc76732b4e6d7be10f6cd588457245537 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 16:14:18 +0800 Subject: [PATCH 004/166] Write the exit-code line and the held-output note in the IDE language --- README.md | 2 +- architecture_explore.md | 12 ++++++------ docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- .../extend/process_executor/file_runner_process.py | 3 +-- .../process_executor/python_task_process_manager.py | 5 ++--- pybreeze/extend/process_executor/queue_pump.py | 5 ----- pybreeze/extend_multi_language/extend_english.py | 3 +++ .../extend_traditional_chinese.py | 2 ++ test/test_utils/test_run_notice.py | 7 +++++++ 10 files changed, 33 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3b4d4319..c0d37ffd 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 729 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 731 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/architecture_explore.md b/architecture_explore.md index 02fafdf4..fe92872f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -50,7 +50,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ┌─────────────────────────────────────────────────────────────────────────┐ │ 基礎層 Foundation │ │ pybreeze/utils/ 18 個工具子套件(純邏輯,可單測) │ - │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 729 鍵) │ + │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 731 鍵) │ └─────────────────────────────────────────────────────────────────────────┘ ▼ 外部子行程:python -m je_api_testka / je_auto_control / je_web_runner / @@ -66,7 +66,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 729 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 731 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示 @@ -157,8 +157,8 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `read_stream_into_queue(stream, queue, buffer_size, encoding, keep_reading)` — reader 執行緒用。行**原樣**進 queue(縮排、行尾、空行都留著);空讀 = EOF 即停,管線被關掉的 `OSError` / `ValueError` 記 debug 後停。超過 `buffer_size` 的長行分段讀進來:用 incremental decoder 解碼(被切斷的多位元組字元接到下一段),段尾的 `\r` 留到下一段(`\r\n` 被切開時不會變成兩個換行);不認得的 encoding 退回 UTF-8 並記 warning - `pump_message_queue(q, append_fn, is_error, max_messages)` — UI 執行緒用。`MAX_MESSAGES_PER_PUMP = 256`:每 tick 只抽一則的話輸出上限只有 ~10 行/秒,聒噪的腳本會爬行;有上界則避免洪水輸出卡住 UI 執行緒。`max_messages=None` 是收尾時一次抽乾。只跳過空字串 - `output_queue()` — 每條管線的 queue 最多 `MAX_QUEUED_MESSAGES`(10,000)則;滿了 reader 就等(每 0.2 秒看一次 `keep_reading`),子行程寫管線也跟著等,跑得跟視窗顯示一樣快,像終端機;以前不設上限,印個不停的腳本會一直吃記憶體,按 Stop 後再一口氣全倒進視窗 -- `ReaderGrace` / `any_alive()` / `OUTPUT_STILL_HELD_NOTE` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 -- 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …` 等 13 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 +- `ReaderGrace` / `any_alive()` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 +- 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …`、`Task exit with code …`、行程仍握著輸出的註明,共 15 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 - 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 - `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`_insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 @@ -363,7 +363,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 729 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 731 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2264 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2265 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index b7049c7e..15109e2b 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -667,3 +667,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2264 tests in 125 files. `ruff check` clean. - **Files**: `pybreeze/extend/process_executor/process_executor_utils.py`, `test/test_utils/test_run_notice.py`, `architecture_explore.md` §18 - **Open items**: none. + +## U-20260924-274 · 2026-09-24 · The exit-code line and the held-output note in the IDE language too · #fix #i18n #run-window + +- **What**: U-20260924-271 translated the run window's `[Error]`/`[Run]`-style notices but left two others English: `Task exit with code 0`, which ends every Python run, and the note that a process the run started still holds its output (`OUTPUT_STILL_HELD_NOTE` in `queue_pump.py`). +- **Fix**: both are `run_notice("exit_code", code=...)` and `run_notice("output_still_held")` (`run_window_exit_code`: `執行結束,結束代碼 {code}`); the constant is gone from `queue_pump.py`. The English reads as before. +- **Tests**: `test_run_notice.py` +1 (both in Traditional Chinese); the run-output, install-menu and code-window tests, which wait for `Task exit with code`, pass unchanged. +- **Result / numbers**: 731 keys in each language. 2265 tests in 125 files. `ruff check` clean. +- **Files**: `pybreeze/extend/process_executor/queue_pump.py`, `pybreeze/extend/process_executor/file_runner_process.py`, `pybreeze/extend/process_executor/python_task_process_manager.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_run_notice.py`, `README.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a845c4f6..b271fd8b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-274 | 2026-09-24 | The exit-code line and the held-output note in the IDE language too | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | | U-20260924-273 | 2026-09-24 | Why a report mail was not sent, in the IDE language | #fix #i18n #mail | [2026-09-b](2026-09-b.md) | | U-20260924-272 | 2026-09-24 | Refactor: the reasons a report mail was not sent come from exception_tags | #refactor #i18n #mail | [2026-09-b](2026-09-b.md) | | U-20260924-271 | 2026-09-24 | A run window's own notices in the IDE language | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | @@ -355,4 +356,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 71 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 72 | diff --git a/pybreeze/extend/process_executor/file_runner_process.py b/pybreeze/extend/process_executor/file_runner_process.py index 78e92d42..d7993386 100644 --- a/pybreeze/extend/process_executor/file_runner_process.py +++ b/pybreeze/extend/process_executor/file_runner_process.py @@ -20,7 +20,6 @@ from PySide6.QtCore import QTimer from pybreeze.extend.process_executor.queue_pump import ( - OUTPUT_STILL_HELD_NOTE, ReaderGrace, any_alive, output_queue, @@ -262,7 +261,7 @@ def _finish(self) -> None: # Drain remaining output directly (not via _pull_text to avoid recursion) self._drain_queues() if any_alive(*readers): - self.main_window.append_output(OUTPUT_STILL_HELD_NOTE, is_error=False, own_line=True) + self.main_window.append_output(run_notice("output_still_held"), is_error=False, own_line=True) after_exit, self._after_exit = self._after_exit, None if self.process is not None: diff --git a/pybreeze/extend/process_executor/python_task_process_manager.py b/pybreeze/extend/process_executor/python_task_process_manager.py index 6c124eca..e41d29f4 100644 --- a/pybreeze/extend/process_executor/python_task_process_manager.py +++ b/pybreeze/extend/process_executor/python_task_process_manager.py @@ -17,7 +17,6 @@ from je_editor.utils.venv_check.check_venv import check_and_choose_venv from pybreeze.extend.process_executor.queue_pump import ( - OUTPUT_STILL_HELD_NOTE, ReaderGrace, any_alive, output_queue, @@ -282,11 +281,11 @@ def exit_program(self): self.read_program_error_output_from_thread = None self.drain_and_display_queue() if any_alive(*readers): - self.main_window.append_output(OUTPUT_STILL_HELD_NOTE, own_line=True) + self.main_window.append_output(run_notice("output_still_held"), own_line=True) if self.process is not None: self.process.terminate() self.main_window.append_output( - f"Task exit with code {self.process.returncode}\n", own_line=True) + run_notice("exit_code", code=self.process.returncode), own_line=True) self.process = None self._remove_script_file() if self.task_done_trigger_function is not None: diff --git a/pybreeze/extend/process_executor/queue_pump.py b/pybreeze/extend/process_executor/queue_pump.py index 3ad4b3b9..fdc99074 100644 --- a/pybreeze/extend/process_executor/queue_pump.py +++ b/pybreeze/extend/process_executor/queue_pump.py @@ -40,11 +40,6 @@ # How often a reader waiting on a full queue checks whether to give up _PUT_WAIT_SECONDS = 0.2 -# Written when a process the run started still holds its output at the end -OUTPUT_STILL_HELD_NOTE = ( - "[A process started by this run still holds its output; what it writes from now on is not shown]\n") - - def output_queue() -> Queue: """A queue for one pipe's output, holding at most ``MAX_QUEUED_MESSAGES`` pieces.""" return Queue(maxsize=MAX_QUEUED_MESSAGES) diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index bae1a13c..570f4cef 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -785,6 +785,9 @@ "run_window_mail_not_sent": "[Mail] The test report was not sent: {reason}", "run_window_no_interpreter": "[Error] No Python interpreter found: {error}", "run_window_package_could_not_start": "[Error] {package} could not start: {reason}", + "run_window_exit_code": "Task exit with code {code}", + "run_window_output_still_held": + "[A process started by this run still holds its output; what it writes from now on is not shown]", } # Why a tool refused its input (pybreeze_ui/error_text.py): in English, the # constants the tools raise diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index e859f1a3..1530c0a1 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -849,6 +849,8 @@ "run_window_mail_not_sent": "[郵件] 沒有寄出測試報告:{reason}", "run_window_no_interpreter": "[錯誤] 找不到 Python 直譯器:{error}", "run_window_package_could_not_start": "[錯誤] 無法啟動 {package}:{reason}", + "run_window_exit_code": "執行結束,結束代碼 {code}", + "run_window_output_still_held": "[這次執行啟動的某個行程仍握著輸出;它之後寫出的內容不會顯示]", } diff --git a/test/test_utils/test_run_notice.py b/test/test_utils/test_run_notice.py index 8b021017..07f92f11 100644 --- a/test/test_utils/test_run_notice.py +++ b/test/test_utils/test_run_notice.py @@ -65,3 +65,10 @@ def test_why_a_report_mail_was_not_sent_in_chinese(monkeypatch): notice.tell(mail_no_user_error) assert told == [("[郵件] 沒有寄出測試報告:沒有設定郵件使用者\n", True)] + + +def test_the_exit_code_and_held_output_lines_in_chinese(monkeypatch): + monkeypatch.setattr(run_notice_mod.language_wrapper, "language_word_dict", CHINESE) + + assert run_notice("exit_code", code=0) == "執行結束,結束代碼 0\n" + assert run_notice("output_still_held").startswith("[這次執行啟動的某個行程仍握著輸出") From 59452846b031165926000870d7b86170052e5fde Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 16:52:09 +0800 Subject: [PATCH 005/166] Bring the Chinese READMEs back in line with README.md --- README/README_zh-CN.md | 507 +++++++++++++++++++------------------ README/README_zh-TW.md | 515 ++++++++++++++++++++------------------ docs/updates/2026-09-b.md | 8 + docs/updates/README.md | 3 +- 4 files changed, 554 insertions(+), 479 deletions(-) diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index e93fa6c5..7e02f3cd 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -3,135 +3,233 @@ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE) [![PySide6](https://img.shields.io/badge/GUI-PySide6-green.svg)](https://doc.qt.io/qtforpython/) +[![Documentation](https://readthedocs.org/projects/pybreeze/badge/?version=latest)](https://pybreeze.readthedocs.io/en/latest/index.html) [English](../README.md) | [繁體中文](README_zh-TW.md) -![主界面](../images/main_gui.png) +**PyBreeze** 是一款专为自动化工程师打造的 Python IDE。Web、API、GUI 和负载测试都在同一个窗口里,旁边还有自动化工作真正需要的日常 HTTP 工具——不用到处找插件,也不用费力考究环境。 -**PyBreeze** 是一款专为自动化工程师打造的 Python IDE。它将 Web、API、GUI 和负载测试自动化整合到单一统一环境中——无需寻找插件、无需复杂的环境配置,打开即可开始自动化。 +![PyBreeze 主窗口](../images/main_window.png) + +*主窗口:APITestka 动作文件中的自动化关键字高亮显示,左侧是项目树,下方是运行/格式检查/调试/终端面板。* --- ## 目录 -- [功能特色](#功能特色) - - [四维自动化](#四维自动化) - - [IDE 核心功能](#ide-核心功能) - - [内置工具](#内置工具) - - [AI 辅助开发](#ai-辅助开发) - - [插件系统](#插件系统) - - [多语言界面](#多语言界面) -- [架构设计](#架构设计) -- [安装方式](#安装方式) +- [截图导览](#截图导览) +- [四维自动化](#四维自动化) +- [内置工具](#内置工具) +- [AI 辅助开发](#ai-辅助开发) +- [插件系统](#插件系统) +- [多语言界面](#多语言界面) +- [架构](#架构) +- [安装](#安装) - [快速开始](#快速开始) - [集成自动化模块](#集成自动化模块) - [项目结构](#项目结构) - [依赖项](#依赖项) +- [测试与 CI](#测试与-ci) - [目标用户](#目标用户) - [许可证](#许可证) --- -## 功能特色 +## 截图导览 + +IDE 的大部分功能都在三个菜单里。**Automation** 运行你的脚本,**Tools** 打开各种工具标签页,**Install** 安装各个模块。 + +| Automation | Tools | Install | +|---|---|---| +| ![Automation 菜单](../images/menu_automation.png) | ![Tools 菜单](../images/menu_tools.png) | ![Install 菜单](../images/menu_install.png) | + +每次自动化运行都在独立的子进程中进行。输出会流回运行窗口,编辑器始终保持响应——stdout 以普通颜色显示,stderr 以红色显示,最后是进程的退出码: + +![运行输出窗口](../images/run_output_window.png) -### 四维自动化 +*一次真实的运行:通过 IDE 的文件运行器调用 PyBreeze 自带的 curl 解析器,生成一个 pytest 测试。* + +--- + +## 四维自动化 PyBreeze 开箱即用,涵盖自动化测试的完整范围: -| 维度 | 模块 | 说明 | +| 维度 | 模块 | 功能 | |---|---|---| -| **Web 自动化** | [WebRunner](https://github.com/Integration-Automation/WebRunner) | 浏览器交互模拟与测试,深度集成浏览器驱动与元素定位器 | -| **API 自动化** | [APITestka](https://github.com/Integration-Automation/APITestka) | RESTful API 开发与测试,内置请求构建器、响应分析器、Mock 服务器及断言验证 | -| **GUI 自动化** | [AutoControl](https://github.com/Integration-Automation/AutoControlGUI) | 桌面应用程序自动化,支持图像识别、坐标定位、键盘鼠标控制及动作录制 | -| **负载与压力测试** | [LoadDensity](https://github.com/Integration-Automation/LoadDensity) | 高并发性能测试引擎,用于监控系统在极端压力下的稳定性 | +| **API** | [APITestka](https://github.com/Integration-Automation/APITestka) | RESTful 测试,包含请求构建器、响应分析器、Mock 服务器与断言 | +| **Web** | [WebRunner](https://github.com/Integration-Automation/WebRunner) | 由浏览器驱动的交互与测试,集成驱动程序与元素定位器 | +| **GUI** | [AutoControl](https://github.com/Integration-Automation/AutoControlGUI) | 桌面自动化:图像识别、坐标、键盘/鼠标控制与录制 | +| **Load** | [LoadDensity](https://github.com/Integration-Automation/LoadDensity) | 高并发性能测试,检验系统在压力下的稳定性 | + +此外还有: + +- **文件自动化** — 通过 [automation-file](https://github.com/Integration-Automation/FileAutomation) 进行文件与目录操作 +- **邮件自动化** — 通过 [MailThunder](https://github.com/Integration-Automation/MailThunder) 投递报告 +- **测试框架** — 通过 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 进行 YAML 驱动的执行 + +每个模块的菜单结构都相同:**Run**(单个脚本、整个目录批量运行,可选择是否以邮件发送报告)、**Help**(文档与 GitHub 以 IDE 内的浏览器标签页打开)、**Project**(生成模板目录),有原生 GUI 的模块还会提供一个 GUI 标签页。 + +### IDE 核心 + +- **识别自动化的语法高亮** — 在 JEditor 的语言支持之上,为 `.json` 注册了 `AT_*` / GUI / Web / Load 关键字集,为 `.yml` 和 `.yaml` 注册了 TestPioneer 的结构定义 +- **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建:标签页、项目树、格式检查、调试器、终端、变量查看器以及 git 客户端面板 +- **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮;动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 +- **报告生成** — 运行后生成 HTML / JSON / XML 报告,可选以邮件发送 +- **集成 JupyterLab** — 以标签页方式启动;项目 venv 中没有 JupyterLab 时会自动安装 +- **虚拟环境感知** — 自动检测并使用 `venv/` 与 `.venv/`;没有虚拟环境时,脚本以 IDE 本身所用的解释器运行 + +--- + +## 内置工具 + +### cURL 导入——复制的请求变成可运行的脚本 + +从浏览器开发者工具粘贴一条 `curl` 命令,选择目标格式。解析器能处理方法、URL、请求头、请求体、basic auth、`-G` 查询参数、`-F` multipart 字段(上传会变成 `files=open(...)`)、`--json` 简写、`-d @file` 请求体以及多行续行。重复的 `-H` 值会按照 HTTP 的方式合并(cookie 用 `; `,其他用 `, `),而不是悄悄只保留最后一个。它从不执行任何东西——纯粹只做解析。 + +| 目标:pytest | 目标:APITestka JSON 动作 | +|---|---| +| ![cURL 导入为 pytest](../images/tool_curl_import.png) | ![cURL 导入为 APITestka 动作](../images/tool_curl_import_action.png) | + +目标格式:Python `requests`、可直接运行的 **pytest** 测试、**APITestka**(Python,或可由 `execute_files` 直接运行的 `[["AT_test_api_method", {...}]]` 动作列表),以及 **LoadDensity** 的 Locust 负载测试。可以复制输出、直接在编辑器标签页中打开,或以正确的扩展名保存。只需一次点击,还能把解析出的 URL 交给 URL 解析器/构建器,或把请求头交给请求头分析器。 + +### HAR 导入——整个会话变成测试套件 + +"Copy as cURL" 只能抓一个请求;**Save all as HAR** 能抓下整个会话。打开导出的文件,每个记录下来的调用都会列出方法、路径、状态码与媒体类型,页面装饰资源(CSS、图片、字体)默认会被过滤掉。 -此外还包含: +![HAR 导入](../images/tool_har_import.png) -- **文件自动化** — 通过 [automation-file](https://github.com/Integration-Automation/FileAutomation) 模块实现自动化文件与目录操作 -- **邮件自动化** — 通过 [MailThunder](https://github.com/Integration-Automation/MailThunder) 实现自动化邮件发送(例如测试报告投递) -- **测试框架** — 通过 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 实现结构化 YAML 驱动的测试执行 +选择想要的请求——或者直接全选列出的项目——用与 cURL 导入器相同的目标格式生成一个脚本。重复的端点会得到编号的测试名称,不会有测试悄悄覆盖另一个;HTTP/2 伪请求头会被去掉,与记录的 cookie 列表重复的 `Cookie` 请求头也会被移除,让每个值只发送一次(同名的 cookie 无法放进字典,则改以请求头发送)。无法变成请求的条目(例如 URL 或方法格式错误)会被跳过,其余照常加载。只选一个请求时,生成的结果与 cURL 导入器完全相同。HAR 就是 JSON,因此只需要标准库,而且从不会替你重放任何请求。 -### IDE 核心功能 +### 响应检查器——粘贴一个响应,读懂其中的一切 -PyBreeze 不仅仅是一个代码编辑器——它是自动化生命周期的指挥中心: +![响应检查器](../images/tool_response_inspector.png) -- **语法高亮** — 内置 Python 语法高亮,针对自动化库(APITestka、AutoControl、WebRunner、LoadDensity 等)提供深度关键字识别。可通过插件添加自定义语法规则。 -- **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建,提供完整的编辑器功能,包含标签页管理、文件树导航与项目工作区支持。 -- **脚本执行** — 直接在 IDE 中执行自动化脚本,并实时显示输出。支持单脚本与多脚本批量执行。 -- **报告生成** — 自动化模块可在测试执行后生成 HTML、JSON 和 XML 报告,并支持可选的电子邮件投递。 -- **集成 JupyterLab** — 在 PyBreeze 中直接以标签页方式启动 JupyterLab,进行交互式笔记本开发。若未安装 JupyterLab 将自动安装。 -- **虚拟环境感知** — 自动检测并使用项目的虚拟环境(`.venv` 或 `venv`)。 +状态码会在 HTTP 参考表中查找,请求头会被解析,JSON 响应体会被格式化,文本中任何位置的 JWT(例如 `Authorization: Bearer` 请求头)都会被解码,其中的时间戳声明以 UTC 显示。每项发现都能在对应的工具标签页中打开,并已预先填好。 -### 内置工具 +### HTTP 请求头分析器——一组请求头实际上在说什么 -- **SSH 客户端** — 完整的 SSH 终端客户端,支持: - - 密码与私钥认证 - - 交互式命令执行 - - 远程文件树查看器,支持 CRUD 操作(创建文件夹、重命名、删除、上传、下载) - - 交互式 TOFU host key 验证,已确认的密钥会持久化到 `~/.pybreeze/ssh_known_hosts` -- **架构图编辑器** — 内置的 WYSIWYG 架构图编辑器: - - 矩形、圆角矩形、椭圆、菱形节点、连线、自由文本 - - 从本地文件或 URL 插入图片(URL 下载会做 SSRF 验证并有大小上限) - - 支持 Mermaid `flowchart` / `graph` 导入 - - 保存/打开为 `.diagram.json`,导出为 PNG 或 SVG - - Undo/redo、对齐、分布、Grid、Snap、Zoom 控制 -- **文件树右键菜单** — 在项目文件树中对任何文件或文件夹右键,可创建文件/文件夹、重命名、删除、复制绝对或相对路径、在系统文件管理器中打开。重命名或删除当前已在编辑器标签页中打开的文件时,标签页会同步更新。 -- **包管理器** — 直接从 IDE 菜单安装自动化模块和构建工具,无需离开编辑器。 -- **集成文档** — 从菜单栏快速访问每个自动化模块的文档和 GitHub 页面。 +![请求头分析器](../images/tool_header_analyzer.png) -### AI 辅助开发 +会报告:重复发送的名称、缺少 `Secure` / `HttpOnly` / `SameSite` 的 `Set-Cookie`、通配符 CORS(以及浏览器会直接拒绝的"通配符加凭据"组合)、短到撑不过一次重启的 HSTS `max-age`、CSP 的 `unsafe-inline` / `unsafe-eval`、产品标识、已弃用的请求头,以及——针对响应——缺少的安全请求头。携带凭据的请求头**只报告名称**;它们的值绝不会进入报告。 -- **AI 代码审查** — 将代码发送到 LLM API 端点进行自动化代码审查。可直接在 IDE 中接受或拒绝建议。 -- **思维链代码审查(prthinker)** — 以 [prthinker](https://github.com/JE-Chen/Code-Review-Framework-Combining-Large-Language-Models-and-Chain-of-Thought-Reasoning) 的审查流程审查正在编辑的文件,或审查一个 Pull Request,输出实时流进运行窗口。一张设置表填完推理后端(审查服务器、OpenAI 兼容端点、Anthropic 或本机模型)、代码托管平台与仓库;密钥与令牌以环境变量交给审查,不会出现在命令行上——那是任务管理器看得到的地方。 -- **CoT(思维链)提示词编辑器** — 创建和管理多步骤 CoT 提示词,用于结构化代码分析,包含: - - 代码审查提示词 - - Code Smell 检测 - - 代码检查分析 - - 逐步分析 - - 摘要生成 -- **Skill 提示词编辑器** — 定义和管理可重复使用的技能型提示词(代码解说、代码审查模板),可发送至 LLM API。 -- **Skill Send GUI** — 在独立的标签页或停靠面板中选择技能提示词模板、按需编辑提示词内容、发送到 LLM API 端点并查看响应。 +### 文本比较 -### 插件系统 +![文本比较](../images/tool_diff.png) -PyBreeze 支持可扩展的插件架构,用于: +比较两份数据——例如预期与实际的 API 响应——得到 unified diff,以及一行新增/删除的摘要。 -- **语法高亮** — 通过插件为任何编程语言添加语法高亮 -- **UI 翻译** — 通过翻译插件添加新的界面语言 -- **运行配置** — 为编译型和解释型语言添加"以...运行"支持(C、C++、Go、Java、Rust 等) -- **插件浏览器** — 直接在 IDE 中从远程仓库浏览并安装插件 +### 日常小工具 -插件会从 `jeditor_plugins/` 目录自动发现加载。完整文档请参阅 [PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md)。 +每个都是一个标签页或停靠面板,底部都有相同的一排按钮:复制/在编辑器中打开/保存到文件。 -**内置插件:** C、C++、Go、Java、Rust 语法高亮与运行支持;法语翻译。 +![JWT 解码器、正则表达式测试器、HTTP 状态码参考、JSON 格式化](../images/tools_montage_a.png) -### 多语言界面 +- **JWT 解码器** — header 与 payload 以格式化的 JSON 显示,`exp` / `iat` / `nbf` / `auth_time` 以易读的 UTC 显示。只做查看:从不验证签名,也从不信任令牌。 +- **正则表达式测试器** — 支持 `IGNORECASE` / `MULTILINE` / `DOTALL` / `VERBOSE`,列出每个匹配及其偏移量、编号分组与命名分组。在模式框中按 Enter 即可运行。无效的模式会显示友好的错误信息,而不会崩溃。 +- **HTTP 状态码参考** — 按状态码前缀或关键字搜索完整的状态码表(来自标准库,因此始终保持最新)。 +- **JSON 格式化** — 格式化或压缩,输入不是 JSON 时给出清楚的验证错误。 -IDE 界面支持多种语言: +![时间戳转换器、哈希生成器、查询字符串/JSON、URL 构建器](../images/tools_montage_b.png) -- **English**(英语,默认) -- **繁体中文** -- 可通过插件添加其他语言 +- **时间戳转换器** — 输入 Unix 时间戳(秒、毫秒、微秒或纳秒,自动识别)或 ISO-8601 日期时间(支持 `Z`、`+08`、`+0800` 或 `+08:00`,任意位数的小数,基本或扩展格式),输出所有 UTC 表示形式。结果确定,与本地时区无关。 +- **哈希生成器** — 一次算出 SHA-256、SHA-512、SHA-1 与 MD5(MD5/SHA-1 以 `usedforsecurity=False` 提供,只为了互操作,绝不用于安全判断)。 +- **Query ⇄ JSON** — `application/x-www-form-urlencoded` 转为格式化的 JSON,也能转回去;重复的键会变成数组,反之亦然。 +- **URL 解析器/构建器** — 把 scheme、host、port、path、query、fragment 与凭据拆成可编辑的 JSON 对象,也能再组回 URL。会自动为 IPv6 字面量加上方括号,并重新编码查询参数。 -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言显示。 +### 图表编辑器——不离开 IDE 就能画架构图 + +![导入 Mermaid 流程图后的图表编辑器](../images/diagram_editor.png) + +*把一段 Mermaid `flowchart` 粘贴到导入器中,自动完成布局。* + +一个基于 `QGraphicsScene` 的所见即所得编辑器:矩形、圆角矩形、椭圆与菱形节点,带边标签的贝塞尔连线,自由文本与图片。Mermaid `flowchart` / `graph` 导入采用 Sugiyama 式布局(分层、减少交叉、跨轴对齐)。可保存和打开 `.diagram.json`,导出为 PNG 或 SVG,支持撤销/重做、对齐、分布、网格、吸附与缩放。从 URL 获取的图片会经过 SSRF 验证并有大小上限。 + +### SSH 客户端——终端与远程文件树并排 + +![SSH 客户端](../images/ssh_client.png) + +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell,以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 + +### 其他 + +- **文件树右键菜单** — 右键即可创建、重命名、删除、复制绝对或相对路径,或在系统的文件管理器中显示该项目(在 Explorer 与 Finder 中会选中该文件)。重命名或删除一个已在编辑器标签页中打开的文件时,标签页会保持同步。 +- **包管理器** — 从菜单安装自动化模块与构建工具,输出显示在运行窗口中。 +- **集成文档** — 每个模块的文档与 GitHub 页面都以 IDE 内的浏览器标签页打开。 --- -## 架构设计 +## AI 辅助开发 + +### AI 代码审查 + +![AI 代码审查客户端](../images/ai_code_review.png) + +*图中为发送前的状态。* 把选中的代码发送到 LLM 端点,然后接受或拒绝建议——统计记录保存在 `~/.pybreeze/response_stats.txt`。URL 会经过 SSRF 验证,连接只会连到经过检查的地址,不跟随重定向,响应体在到达面板之前会被限制大小。 + +### 思维链代码审查(prthinker) + +对正在编辑的文件或一个 Pull Request 运行 [prthinker](https://github.com/JE-Chen/Code-Review-Framework-Combining-Large-Language-Models-and-Chain-of-Thought-Reasoning) 流程,输出实时流入运行窗口。 + +![prthinker 设置](../images/prthinker_setting.png) + +一张设置表就包含推理后端(`remote`、`local`、OpenAI 兼容、Anthropic、Gemini、Cohere、Mistral、`claude-cli`、`codex-cli`)、代码托管平台(GitHub / GitLab / Gitea)与仓库。**密钥与令牌以环境变量交给审查,绝不放在命令行上**——那是进程列表看得到的地方——并且在日志中会被遮蔽。模型名称会交给所选的后端。规则检索(RAG)默认为 `off`,除非设为 `remote`,此时会向 prthinker 服务器的 `/rag` 查询:prthinker 的本地规则索引随它的仓库提供,而不在从仓库安装的包里。审查以 `Python Env` 中选择的解释器运行,因此 PyBreeze 本身可以停留在比 prthinker 所需的 3.12 更旧的 Python 上。 + +### CoT 提示词编辑器 + +![CoT 提示词编辑器](../images/cot_prompt_editor.png) + +创建和管理多步骤的审查链:第一次摘要 → 第一次代码审查 → 对该审查的评判 → linter → 代码坏味道检测 → 逐步分析 → 总摘要 → 对摘要的评判。每一步都会引用它所需的前面步骤的回答。文件受到监视,因此外部的编辑会立即显示出来。 + +从 **Tools → AI → CoT Code Review**(一个标签页,或从 Dock 菜单打开停靠面板)运行这条审查链:粘贴代码,填入端点 URL,每一步的回答一到达就会出现在选择器中。 + +### Skill 提示词编辑器与 Skill Send + +| Skill 提示词编辑器 | Skill Send | +|---|---| +| ![Skill 提示词编辑器](../images/skill_prompt_editor.png) | ![Skill Send](../images/skills_send.png) | + +定义可重复使用的技能提示词(代码解说、代码审查),然后选择一个,按需编辑,再从专用的标签页或停靠面板发送到 LLM 端点。*两张图都是发送前的状态——拍摄这些截图时没有连接任何端点。* + +--- + +## 插件系统 + +PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_plugins/` 目录自动发现插件。插件可以注册: + +- **语法高亮** — 任何语言的关键字集与规则 +- **界面翻译** — 新的界面语言 +- **运行配置** — 为解释型(`go run main.go`)与编译型(`gcc main.c -o main` 后再运行)语言提供"Run with…",通过 PyBreeze 的 `FileRunnerProcess` 执行,编译产物在运行后会被清理 +- **插件浏览器** — 在 IDE 内从远程仓库浏览并安装插件 + +已加载的插件会出现在它们自己的 **Plugins** 菜单下,包含一个 About 项与一个运行动作,标签上注明它能运行的文件扩展名。[PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md) 介绍 PyBreeze 额外提供的内容,并链接到 JEditor 的指南,那里有完整的 API 与示例(C、C++、Go、Java、Rust,以及一份法语翻译)。 + +--- + +## 多语言界面 + +- **English**(默认) +- **繁體中文**(Traditional Chinese) + +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 731 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 + +--- + +## 架构 ```mermaid flowchart TB UI["PyBreeze UI · PySide6"] - subgraph Editor["JEditor 基础编辑器"] + subgraph Editor["JEditor (Base Editor)"] direction LR - E1["代码编辑器 + 标签页"] - E2["文件树"] - E3["语法高亮"] - E4["插件系统"] + E1["Code Editor + Tabs"] + E2["File Tree"] + E3["Syntax Highlighting"] + E4["Plugin System"] end - subgraph Automation["自动化菜单"] + subgraph Automation["Automation Menu"] direction LR A1["APITestka"] A2["AutoControl"] @@ -142,7 +240,7 @@ flowchart TB A7["TestPioneer"] end - subgraph Executors["子进程执行器 · TaskProcessManager"] + subgraph Executors["Subprocess Executors · TaskProcessManager"] direction LR X1["je_api_testka"] X2["je_auto_control"] @@ -153,21 +251,20 @@ flowchart TB X7["test_pioneer"] end - subgraph Tools["工具"] + subgraph Tools["Tools"] direction LR T1["SSH · paramiko"] - T2["AI 代码审查"] - T3["CoT 提示词编辑器"] - T4["Skill 提示词编辑器"] - T5["Skill Send GUI"] - T6["架构图编辑器"] - T7["JupyterLab"] + T2["AI Code Review"] + T3["Prompt Editors"] + T4["Diagram Editor"] + T5["HTTP Toolbelt"] + T6["JupyterLab"] end - subgraph Install["安装菜单"] + subgraph Install["Install Menu"] direction LR - I1["模块安装器"] - I2["构建工具"] + I1["Module Installers"] + I2["Build Tools"] end UI --> Editor @@ -184,41 +281,13 @@ flowchart TB A7 --> X7 ``` -PyBreeze 采用模块化架构: - -``` -PyBreeze UI (PySide6) -├── JEditor(基础编辑器引擎) -│ ├── 代码编辑器与标签页 -│ ├── 文件树导航 -│ ├── 语法高亮引擎 -│ └── 插件系统 -├── 自动化菜单 -│ ├── APITestka ──→ APITestka 执行器 ──→ je_api_testka -│ ├── AutoControl ──→ AutoControl 执行器 ──→ je_auto_control -│ ├── WebRunner ──→ WebRunner 执行器 ──→ je_web_runner -│ ├── LoadDensity ──→ LoadDensity 执行器 ──→ je_load_density -│ ├── FileAutomation ──→ FileAutomation 执行器 ──→ automation-file -│ ├── MailThunder ──→ MailThunder 执行器 ──→ je-mail-thunder -│ └── TestPioneer ──→ TestPioneer 执行器 ──→ test_pioneer -├── 工具 -│ ├── SSH 客户端(paramiko) -│ ├── AI 代码审查客户端 -│ ├── CoT 提示词编辑器 -│ ├── Skill 提示词编辑器 -│ ├── Skill Send GUI -│ ├── 架构图编辑器(WYSIWYG、Mermaid 导入、PNG/SVG 导出) -│ └── JupyterLab 集成 -└── 安装菜单 - ├── 自动化模块安装器 - └── 构建工具安装器 -``` +**编辑器进程从不运行你的脚本。** 每个自动化模块都以 `python -m ` 的方式、用项目的解释器、以 `shell=False` 启动。两个守护线程把 stdout 与 stderr 读入线程安全的队列;一个 100 ms 的 `QTimer` 以有上限的批次把它们送到 UI 线程。脚本崩溃、卡住或无限打印,都不会拖垮 IDE。 -每个自动化模块都通过 `PythonTaskProcessManager` 在独立的子进程中执行,提供进程隔离,防止崩溃影响 IDE。 +逐个模块的代码导览请参阅 [architecture_explore.md](../architecture_explore.md)。 --- -## 安装方式 +## 安装 ### 从 PyPI 安装 @@ -236,103 +305,48 @@ pip install -r requirements.txt ### 系统要求 -- **Python**:3.10 或更高版本 +- **Python**:3.10 – 3.14 - **操作系统**:Windows、macOS、Linux -- **GUI 框架**:PySide6 6.11.2(自动安装) +- **GUI**:PySide6 6.11.2(自动安装) --- ## 快速开始 -### 通过命令行运行 - ```bash -python -m pybreeze +python -m pybreeze # 命令行 +python exe/start_pybreeze.py # 从 exe 目录运行 ``` -### 通过 Python 脚本运行 - ```python from pybreeze import start_editor -start_editor() +start_editor() # 默认 dark_amber 主题 +start_editor(theme="dark_teal.xml") # 任意 qt_material 主题 ``` -### 从 exe 目录运行 +启动后: -```bash -python exe/start_pybreeze.py -``` - -启动后,您可以: - -1. **编写自动化脚本** — 在编辑器中享有语法感知的自动补全 -2. **执行脚本** — 通过 `自动化` 菜单,选择目标模块(APITestka、WebRunner 等) -3. **查看结果** — 在集成式输出面板中查看 -4. **生成报告** — 支持 HTML/JSON/XML 格式 -5. **发送报告** — 使用 MailThunder 集成功能通过电子邮件发送 +1. **编写** — 在编辑器中编写自动化脚本,自动化关键字会随输入高亮 +2. **运行** — 从 `Automation` 菜单运行,选择目标模块 +3. **查看** — 输出实时流入运行窗口 +4. **生成** — 生成 HTML / JSON / XML 报告 +5. **发送** — 通过 MailThunder 集成以电子邮件发送报告 --- ## 集成自动化模块 -### APITestka — API 测试 - -- HTTP 方法测试(GET、POST、PUT、DELETE 等) -- 通过 httpx 支持异步 HTTP -- 使用 Flask 创建 Mock 服务器 -- 报告生成(HTML、JSON、XML) -- 基于调度器的事件触发 -- Socket 服务器支持 - -### AutoControl — GUI 自动化 - -- 鼠标控制(点击、拖拽、滚动、位置追踪) -- 键盘模拟(输入、快捷键、按键按下/释放) -- 图像识别与定位点击 -- 屏幕截图 -- 动作录制与回放 -- Shell 命令执行 -- 进程管理 - -### WebRunner — Web 自动化 - -- 浏览器驱动集成 -- 元素定位与交互 -- 基于 Web 的测试脚本 -- 报告生成 - -### LoadDensity — 负载测试 - -- 并发请求模拟 -- 性能指标收集 -- 压力测试场景管理 -- 报告生成 - -### MailThunder — 邮件自动化 - -- SMTP 邮件发送 -- HTML 报告投递 -- 附件支持 -- 基于环境变量的配置 - -### TestPioneer — 测试框架 - -- 基于 YAML 的测试定义 -- 模板生成 -- 结构化测试执行 - -### File Automation — 文件自动化 - -- 自动化文件与目录操作 -- 批量文件处理 - -### prthinker — 思维链代码审查 - -- 审查正在编辑的文件,或审查 GitHub / GitLab / Gitea 上的 Pull Request -- 推理后端可选:审查服务器、OpenAI 兼容端点、Anthropic,或本机模型 -- 设置存放在用户目录的 `~/.pybreeze/prthinker_setting.json`;密钥以环境变量交给审查 -- 从它自己的源码文件夹安装(**安装 ▸ 自动化 ▸ 安装 prthinker**),需要 Python 3.12 以上 +| 模块 | 功能 | +|---|---| +| **APITestka** | HTTP 方法、通过 httpx 的异步请求、Flask Mock 服务器、HTML/JSON/XML 报告、调度器触发、socket 服务器、JSON-schema 与 JSONPath 断言、SLA 检查、录制回放 cassette | +| **AutoControl** | 鼠标(点击、拖拽、滚动、位置)、键盘(输入、快捷键、按下/释放)、图像识别与定位点击、截图、录制与回放、shell 与进程控制 | +| **WebRunner** | 浏览器驱动集成、元素定位与交互、Web 测试脚本、报告 | +| **LoadDensity** | 并发请求模拟、性能指标、压力场景管理、报告 | +| **MailThunder** | SMTP 发送、HTML 报告投递、附件、环境变量配置 | +| **TestPioneer** | YAML 测试定义、模板生成、结构化执行 | +| **File Automation** | 自动化文件与目录操作、批量处理 | +| **prthinker** | 对文件或 Pull Request 进行思维链代码审查;设置保存在 `~/.pybreeze/prthinker_setting.json`;通过 `Install ▸ Automation ▸ Install prthinker` 从它自己的源码文件夹安装(需要 Python 3.12+) | --- @@ -341,39 +355,42 @@ python exe/start_pybreeze.py ``` PyBreeze/ ├── pybreeze/ -│ ├── __init__.py # 公开 API(start_editor、插件 re-export) -│ ├── __main__.py # 入口点(python -m pybreeze) +│ ├── __init__.py # 公开 API(start_editor、插件 re-export) +│ ├── __main__.py # 入口点(python -m pybreeze) │ ├── extend/ -│ │ ├── mail_thunder_extend/ # 测试后邮件报告发送 -│ │ ├── process_executor/ # 各自动化模块的子进程管理器 -│ │ │ ├── api_testka/ -│ │ │ ├── auto_control/ -│ │ │ ├── file_automation/ -│ │ │ ├── load_density/ -│ │ │ ├── mail_thunder/ -│ │ │ ├── test_pioneer/ -│ │ │ └── web_runner/ -│ │ └── process_executor/python_task_process_manager.py -│ ├── extend_multi_language/ # 内置翻译(英语、繁体中文) +│ │ ├── process_executor/ # 子进程隔离层 +│ │ │ ├── python_task_process_manager.py # TaskProcessManager(核心) +│ │ │ ├── process_executor_utils.py # build_process / start_process +│ │ │ ├── file_runner_process.py # 插件运行配置(任意语言) +│ │ │ ├── queue_pump.py # 共用的管道读取器 + QTimer 排空 +│ │ │ ├── api_testka/ auto_control/ web_runner/ +│ │ │ ├── load_density/ file_automation/ mail_thunder/ +│ │ │ ├── test_pioneer/ prthinker/ +│ │ ├── mail_thunder_extend/ # 测试后邮件报告钩子 +│ │ └── prthinker_extend/ # prthinker 设置与参数组装 +│ ├── extend_multi_language/ # 内置多语言(英语、繁体中文) │ ├── pybreeze_ui/ -│ │ ├── editor_main/ # 主窗口(扩展 JEditor)+ 文件树右键菜单 -│ │ ├── connect_gui/ssh/ # SSH 客户端组件(TOFU host key 验证) -│ │ ├── diagram_editor/ # WYSIWYG 架构图编辑器 -│ │ ├── extend_ai_gui/ # AI 代码审查与提示词编辑器 -│ │ ├── jupyter_lab_gui/ # JupyterLab 集成 -│ │ ├── menu/ # 菜单栏构建 -│ │ ├── syntax/ # 自动化关键字定义 -│ │ └── show_code_window/ # 代码显示组件 -│ └── utils/ # 日志、异常处理、文件处理、包管理 -├── exe/ # 独立启动器与构建配置 -├── docs/ # Sphinx 文档源码 -├── test/ # 单元测试 -├── images/ # 截图 -├── architecture_diagram/ # 架构图 -├── PLUGIN_GUIDE.md # 插件开发文档 -├── pyproject.toml # 包配置 -├── requirements.txt # 运行时依赖项 -└── dev_requirements.txt # 开发依赖项 +│ │ ├── editor_main/ # 主窗口 + 文件树右键菜单 +│ │ ├── menu/ # Automation / Install / Tools / Plugins 菜单 +│ │ ├── tools_gui/ # cURL、HAR、JWT、diff、regex …… 工具标签页 +│ │ ├── diagram_editor/ # 所见即所得图表编辑器 +│ │ ├── extend_ai_gui/ # CoT 审查、提示词编辑器、skill send +│ │ ├── connect_gui/ # SSH 终端 + SFTP 文件树、AI 审查客户端 +│ │ ├── jupyter_lab_gui/ # JupyterLab 标签页 +│ │ ├── show_code_window/ # CodeWindow(运行输出) +│ │ ├── dialog/ # prthinker 设置对话框 +│ │ └── syntax/ # 自动化关键字定义 +│ └── utils/ # curl/HAR 解析、请求头、JWT、哈希、 +│ # URL 验证、日志、异常 …… +├── exe/ # 独立启动器与构建配置 +├── docs/ # Sphinx 文档源码 +├── test/ # 单元测试(test_utils)+ 启动测试 +├── images/ # 截图 +├── architecture_explore.md # 逐个模块的架构说明 +├── PLUGIN_GUIDE.md # 插件开发文档 +├── pyproject.toml # 包配置(稳定版) +├── dev.toml # 包配置(开发通道) +└── requirements.txt # 运行时依赖项 ``` --- @@ -384,35 +401,51 @@ PyBreeze/ | 包 | 用途 | |---|---| -| `PySide6` (6.11.2) | GUI 框架(Qt for Python)| +| `PySide6` (6.11.2) | GUI 框架(Qt for Python) | | `je-editor` | 基础代码编辑器引擎 | | `je_api_testka` | API 测试自动化 | -| `je_auto_control` | GUI/桌面自动化 | +| `je_auto_control` | GUI/桌面自动化 | | `je_web_runner` | Web 浏览器自动化 | | `je_load_density` | 负载与压力测试 | | `je-mail-thunder` | 邮件自动化 | | `automation-file` | 文件操作自动化 | | `test_pioneer` | 基于 YAML 的测试框架 | | `paramiko` | SSH 客户端支持 | -| `jupyterlab` | 集成式笔记本环境 | +| `jupyterlab` | 集成的笔记本环境 | ### 开发 -`build`、`twine`、`sphinx`、`sphinx-rtd-theme`、`auto-py-to-exe` +`build`、`twine`、`sphinx`、`sphinx-rtd-theme`、`auto-py-to-exe`、`pytest`、`pytest-cov`、`hypothesis`、`ruff` + +--- + +## 测试与 CI + +```bash +python -m pip install -r dev_requirements.txt +python -m pytest test/test_utils/ -v --tb=short +``` + +- **单元测试** — `test/test_utils/`,覆盖纯逻辑层(curl 与 HAR 解析、请求头分析、SSRF 验证、JWT、哈希、时间戳、差异比较),加上通过 `QT_QPA_PLATFORM=offscreen` 进行的无界面 Qt 组件测试,以及针对解析器的 Hypothesis 属性测试 +- **启动测试** — `test/unit_test/start_automation/` 以调试模式启动 IDE,验证它能正常启动并干净地退出 +- **CI** — GitHub Actions 在 Windows 上跑 Python 3.10 – 3.14,每次 push 与 PR 都会运行,另有每晚一次的运行 +- **静态分析** — SonarCloud、Codacy 与 Bandit --- ## 目标用户 -- **Python 开发者** — 一个轻量、专用的环境,用于构建自动化脚本,无需承受重量级通用 IDE 的负担 -- **SDET(测试开发工程师)** — 需要在同一工具中同时维护 Web、API 和性能测试的专业人士 -- **自动化初学者** — 一个友好的 IDE,通过零配置环境降低 Python 自动化的入门门槛 -- **DevOps 团队** — 一个在 CI/CD 流水线中快速构建和调试集成测试套件的平台 +- **Python 开发者** — 一个轻量、专用的自动化脚本环境,没有通用 IDE 的负担 +- **SDET(测试开发工程师)** — 用一个工具同时维护 Web、API 与性能测试 +- **自动化初学者** — 零配置的环境设置,每个模块都有菜单 +- **DevOps 团队** — 构建和调试将要进入 CI/CD 的集成测试套件的地方 --- ## 许可证 -本项目采用 MIT 许可证——详情请参阅 [LICENSE](../LICENSE) 文件。 +MIT — 详见 [LICENSE](../LICENSE)。Copyright (c) 2022 JE-Chen + +--- -Copyright (c) 2022 JE-Chen +截图均取自 Windows 11 上实际的 PyBreeze 组件,使用默认的 `dark_amber` 主题;各工具中的示例数据都是经真实代码路径处理的真实输入。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 76acc8f0..6eeef2f1 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -3,135 +3,233 @@ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE) [![PySide6](https://img.shields.io/badge/GUI-PySide6-green.svg)](https://doc.qt.io/qtforpython/) +[![Documentation](https://readthedocs.org/projects/pybreeze/badge/?version=latest)](https://pybreeze.readthedocs.io/en/latest/index.html) [English](../README.md) | [简体中文](README_zh-CN.md) -![主介面](../images/main_gui.png) +**PyBreeze** 是一款專為自動化工程師打造的 Python IDE。Web、API、GUI 與負載測試都在同一個視窗裡,旁邊還有自動化工作實際需要的日常 HTTP 工具——不必四處找插件,也不必挖掘環境設定。 -**PyBreeze** 是一款專為自動化工程師打造的 Python IDE。它將 Web、API、GUI 和負載測試自動化整合到單一統一環境中——無需尋找插件、無需複雜的環境設定,開啟即可開始自動化。 +![PyBreeze 主視窗](../images/main_window.png) + +*主視窗:APITestka 動作檔中的自動化關鍵字已高亮,左側是專案樹,下方是執行/格式檢查/除錯/終端機面板。* --- ## 目錄 -- [功能特色](#功能特色) - - [四維自動化](#四維自動化) - - [IDE 核心功能](#ide-核心功能) - - [內建工具](#內建工具) - - [AI 輔助開發](#ai-輔助開發) - - [插件系統](#插件系統) - - [多語言介面](#多語言介面) -- [架構設計](#架構設計) -- [安裝方式](#安裝方式) +- [截圖導覽](#截圖導覽) +- [四維自動化](#四維自動化) +- [內建工具](#內建工具) +- [AI 輔助開發](#ai-輔助開發) +- [插件系統](#插件系統) +- [多語言介面](#多語言介面) +- [架構](#架構) +- [安裝](#安裝) - [快速開始](#快速開始) -- [整合自動化模組](#整合自動化模組) +- [整合的自動化模組](#整合的自動化模組) - [專案結構](#專案結構) -- [依賴項目](#依賴項目) +- [相依套件](#相依套件) +- [測試與 CI](#測試與-ci) - [目標使用者](#目標使用者) - [授權條款](#授權條款) --- -## 功能特色 +## 截圖導覽 + +IDE 的大部分功能集中在三個選單(繁體中文介面顯示為括號內的名稱)。**Automation**(自動化)執行你的腳本,**Tools**(工具)開啟各種工具分頁,**Install**(安裝)下載安裝各個模組。 + +| Automation | Tools | Install | +|---|---|---| +| ![Automation 選單](../images/menu_automation.png) | ![Tools 選單](../images/menu_tools.png) | ![Install 選單](../images/menu_install.png) | + +每次自動化執行都在獨立的子行程中進行。輸出會即時傳回執行視窗,編輯器同時保持可操作——stdout 以一般顏色顯示,stderr 以紅色顯示,最後附上行程的結束代碼: + +![執行輸出視窗](../images/run_output_window.png) -### 四維自動化 +*一次實際的執行:透過 IDE 的檔案執行器呼叫 PyBreeze 自己的 curl 解析器,產生一支 pytest 測試。* + +--- + +## 四維自動化 PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: -| 維度 | 模組 | 說明 | +| 維度 | 模組 | 功能 | |---|---|---| -| **Web 自動化** | [WebRunner](https://github.com/Integration-Automation/WebRunner) | 瀏覽器互動模擬與測試,深度整合瀏覽器驅動與元素定位器 | -| **API 自動化** | [APITestka](https://github.com/Integration-Automation/APITestka) | RESTful API 開發與測試,內建請求建構器、回應分析器、Mock 伺服器及斷言驗證 | -| **GUI 自動化** | [AutoControl](https://github.com/Integration-Automation/AutoControlGUI) | 桌面應用程式自動化,支援圖像辨識、座標定位、鍵盤滑鼠控制及動作錄製 | -| **負載與壓力測試** | [LoadDensity](https://github.com/Integration-Automation/LoadDensity) | 高併發效能測試引擎,用於監控系統在極端壓力下的穩定性 | +| **API** | [APITestka](https://github.com/Integration-Automation/APITestka) | RESTful 測試,內建請求建構器、回應分析器、Mock 伺服器與斷言 | +| **Web** | [WebRunner](https://github.com/Integration-Automation/WebRunner) | 以瀏覽器驅動的互動與測試,整合驅動程式與元素定位器 | +| **GUI** | [AutoControl](https://github.com/Integration-Automation/AutoControlGUI) | 桌面自動化,支援圖像辨識、座標、鍵盤/滑鼠控制與錄製 | +| **Load** | [LoadDensity](https://github.com/Integration-Automation/LoadDensity) | 高併發效能測試,檢驗系統在壓力下的穩定性 | + +此外還有: + +- **檔案自動化** — 透過 [automation-file](https://github.com/Integration-Automation/FileAutomation) 進行檔案與目錄操作 +- **郵件自動化** — 透過 [MailThunder](https://github.com/Integration-Automation/MailThunder) 寄送報告 +- **測試框架** — 透過 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 以 YAML 驅動執行 + +每個模組的選單結構都一樣:**Run**(單一腳本、整個目錄批次執行,可選擇是否寄出報告郵件)、**Help**(文件與 GitHub 頁面以 IDE 內的瀏覽器分頁開啟)、**Project**(建立範本目錄),以及在有提供時的原生 GUI 分頁。 + +### IDE 核心 + +- **認得自動化的語法高亮** — 在 JEditor 的語言支援之上,`AT_*`/GUI/Web/Load 關鍵字集註冊給 `.json`,TestPioneer 的結構描述註冊給 `.yml` 與 `.yaml` +- **程式碼編輯器** — 以 [JEditor](https://github.com/Integration-Automation/JEDITOR) 為基礎:分頁、專案樹、格式檢查、除錯器、終端機、變數檢視器與 git 用戶端面板 +- **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕;動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 +- **報告產生** — 執行後產生 HTML/JSON/XML 報告,並可選擇以電子郵件寄送 +- **整合 JupyterLab** — 以分頁方式啟動;專案虛擬環境中沒有 JupyterLab 時會自動安裝 +- **虛擬環境感知** — 自動偵測並使用 `venv/` 與 `.venv/`;都沒有時,腳本會以 IDE 本身使用的直譯器執行 + +--- + +## 內建工具 + +### cURL 匯入 — 複製下來的請求變成可執行的腳本 + +從瀏覽器開發者工具貼上一段 `curl` 指令,再選擇輸出目標。解析器能處理方法、URL、標頭、本文、Basic 驗證、`-G` 查詢參數、`-F` multipart 欄位(上傳檔案會變成 `files=open(...)`)、`--json` 簡寫、`-d @file` 本文,以及多行接續。重複的 `-H` 值會照 HTTP 的方式合併(cookie 用 `; `,其他用 `, `),而不是默默只留下最後一個。過程中不會執行任何東西——純粹是解析。 + +| 目標:pytest | 目標:APITestka JSON 動作 | +|---|---| +| ![cURL 匯入為 pytest](../images/tool_curl_import.png) | ![cURL 匯入為 APITestka 動作](../images/tool_curl_import_action.png) | + +輸出目標:Python `requests`、可直接執行的 **pytest** 測試、**APITestka**(Python,或可由 `execute_files` 直接執行的 `[["AT_test_api_method", {...}]]` 動作清單),以及 **LoadDensity** 的 Locust 負載測試。輸出可以複製、直接開到編輯器分頁,或以正確的副檔名儲存。只要按一下,也能把解析出的 URL 交給 URL 解析/建構器,或把標頭交給標頭分析器。 + +### HAR 匯入 — 整段工作階段變成測試套件 + +「Copy as cURL」只抓一個請求;**Save all as HAR** 則抓下整段工作階段。開啟匯出檔後,每個記錄下來的呼叫都會列出方法、路徑、狀態碼與媒體類型,頁面裝飾類資源(CSS、圖片、字型)預設會被濾掉。 -此外還包含: +![HAR 匯入](../images/tool_har_import.png) -- **檔案自動化** — 透過 [automation-file](https://github.com/Integration-Automation/FileAutomation) 模組實現自動化檔案與目錄操作 -- **郵件自動化** — 透過 [MailThunder](https://github.com/Integration-Automation/MailThunder) 實現自動化郵件寄送(例如測試報告傳遞) -- **測試框架** — 透過 [TestPioneer](https://github.com/Integration-Automation/TestPioneer) 實現結構化 YAML 驅動的測試執行 +選擇需要的項目——或直接取用全部列出的項目——就能用與 cURL 匯入相同的輸出目標產生一支腳本。重複的端點會得到編號過的測試名稱,不會有測試默默蓋掉另一個;HTTP/2 虛擬標頭會被移除,與記錄中 cookie 清單重複的 `Cookie` 標頭也會拿掉,讓每個值只送出一次(同名的 cookie 無法放進字典,改以標頭送出)。無法變成請求的項目,例如 URL 或方法格式錯誤的項目,會被略過,其餘照常載入。只選一個請求時,產生的結果與 cURL 匯入完全相同。HAR 是 JSON,因此只需要標準函式庫,而且不會替你重播任何請求。 -### IDE 核心功能 +### Response Inspector — 貼上回應,讀出裡面的一切 -PyBreeze 不僅僅是一個程式碼編輯器——它是自動化生命週期的指揮中心: +![Response Inspector](../images/tool_response_inspector.png) + +狀態碼會到 HTTP 參考表中查詢,標頭會被解析,JSON 本文會格式化顯示,文字中任何位置的 JWT(例如 `Authorization: Bearer` 標頭)都會被解碼,時間戳記類的宣告以 UTC 顯示。每項發現都能在對應的工具分頁中開啟,並預先填好內容。 + +### HTTP 標頭分析器 — 一段標頭實際在說什麼 + +![標頭分析器](../images/tool_header_analyzer.png) + +會回報:送出不只一次的名稱、缺少 `Secure`/`HttpOnly`/`SameSite` 的 `Set-Cookie` 項目、萬用字元 CORS(以及瀏覽器會直接拒絕的「萬用字元加憑證」組合)、短到撐不過重新啟動的 HSTS `max-age`、CSP 的 `unsafe-inline`/`unsafe-eval`、產品版本標語、已淘汰的標頭,以及——針對回應——缺少的安全標頭。攜帶憑證的標頭**只回報名稱**;它們的值絕不會進入報告。 + +### 文字比對 + +![文字比對](../images/tool_diff.png) + +比較兩段內容——例如預期與實際的 API 回應——得到 unified diff,以及一行新增/刪除摘要。 + +### 日常小工具 + +每個都是分頁或停駐面板,底部都有同樣的一排:複製/在編輯器開啟/儲存成檔案。 + +![JWT 解碼器、正規表示式測試器、HTTP 狀態碼參考、JSON 格式化](../images/tools_montage_a.png) + +- **JWT 解碼器** — 標頭與酬載以格式化的 JSON 顯示,`exp`/`iat`/`nbf`/`auth_time` 轉成易讀的 UTC。只做檢視:絕不驗證簽章,也絕不信任權杖。 +- **正規表示式測試器** — 支援 `IGNORECASE`/`MULTILINE`/`DOTALL`/`VERBOSE`,列出每個比對結果的位移、編號群組與具名群組。在樣式欄按 Enter 即執行。無效的樣式會顯示友善的錯誤訊息,不會當掉。 +- **HTTP 狀態碼參考** — 以代碼前綴或關鍵字搜尋完整的狀態碼表(資料來自標準函式庫,因此會保持最新)。 +- **JSON 格式化** — 格式化或壓縮,輸入不是 JSON 時會給出清楚的驗證錯誤。 + +![時間戳記轉換器、雜湊產生器、Query/JSON、URL 建構器](../images/tools_montage_b.png) + +- **時間戳記轉換器** — 輸入 Unix epoch(秒、毫秒、微秒或奈秒,自動判斷)或 ISO-8601 日期時間(可帶 `Z`、`+08`、`+0800` 或 `+08:00`,小數位數不限,基本或延伸格式皆可),輸出所有 UTC 表示法。結果固定,不受本機時區影響。 +- **雜湊產生器** — 同時計算 SHA-256、SHA-512、SHA-1 與 MD5(MD5/SHA-1 以 `usedforsecurity=False` 提供互通用途,絕不用於安全判斷)。 +- **Query ⇄ JSON** — `application/x-www-form-urlencoded` 轉成格式化的 JSON,也能轉回來;重複的鍵會變成陣列,反之亦然。 +- **URL 解析器/建構器** — 把 scheme、主機、連接埠、路徑、查詢、片段與憑證拆成可編輯的 JSON 物件,也能組回 URL。會自動為 IPv6 位址加上方括號,並重新編碼查詢參數。 + +### 圖表編輯器 — 不離開 IDE 就能畫架構圖 + +![匯入 Mermaid 流程圖後的圖表編輯器](../images/diagram_editor.png) + +*把 Mermaid `flowchart` 貼進匯入器後自動排版的結果。* + +以 `QGraphicsScene` 打造的所見即所得編輯器:矩形、圓角矩形、橢圓與菱形節點,可加上連線標籤的貝茲曲線連線,還有自由文字與圖片。Mermaid `flowchart`/`graph` 匯入會執行 Sugiyama 式排版(分層、減少交叉、跨軸對齊)。可儲存與開啟 `.diagram.json`,匯出為 PNG 或 SVG,並支援復原/重做、對齊、均分、格線、吸附與縮放。從 URL 下載的圖片會經過 SSRF 驗證並有大小上限。 + +### SSH 用戶端 — 終端機與遠端檔案樹並排 + +![SSH 用戶端](../images/ssh_client.png) + +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell,以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 + +### 其他 + +- **檔案樹右鍵選單** — 按右鍵即可建立、重新命名、刪除、複製絕對或相對路徑,或在系統的檔案管理員中顯示該項目(在 Explorer 與 Finder 中會選取該檔案)。重新命名或刪除已在編輯器分頁中開啟的檔案時,分頁會同步更新。 +- **套件管理員** — 從選單安裝自動化模組與建置工具,輸出顯示在執行視窗中。 +- **整合文件** — 每個模組的文件與 GitHub 頁面都以 IDE 內的瀏覽器分頁開啟。 + +--- -- **語法高亮** — 內建 Python 語法高亮,針對自動化函式庫(APITestka、AutoControl、WebRunner、LoadDensity 等)提供深度關鍵字識別。可透過插件新增自訂語法規則。 -- **程式碼編輯器** — 基於 [JEditor](https://github.com/Integration-Automation/JEDITOR) 構建,提供完整的編輯器功能,包含分頁管理、檔案樹瀏覽與專案工作區支援。 -- **腳本執行** — 直接在 IDE 中執行自動化腳本,並即時顯示輸出。支援單一腳本與多腳本批次執行。 -- **報告生成** — 自動化模組可在測試執行後生成 HTML、JSON 和 XML 報告,並支援可選的電子郵件傳遞。 -- **整合 JupyterLab** — 在 PyBreeze 中直接以分頁方式啟動 JupyterLab,進行互動式筆記本開發。若未安裝 JupyterLab 將自動安裝。 -- **虛擬環境感知** — 自動偵測並使用專案的虛擬環境(`.venv` 或 `venv`)。 +## AI 輔助開發 -### 內建工具 +### AI 程式碼審查 -- **SSH 用戶端** — 完整的 SSH 終端用戶端,支援: - - 密碼與私鑰驗證 - - 互動式指令執行 - - 遠端檔案樹檢視器,支援 CRUD 操作(建立資料夾、重新命名、刪除、上傳、下載) - - 互動式 TOFU host key 驗證,已確認的金鑰會持久化到 `~/.pybreeze/ssh_known_hosts` -- **架構圖編輯器** — 內建的 WYSIWYG 架構圖編輯器: - - 矩形、圓角矩形、橢圓、菱形節點、連線、自由文字 - - 從本地檔案或 URL 插入圖片(URL 下載會做 SSRF 驗證並有大小上限) - - 支援 Mermaid `flowchart` / `graph` 匯入 - - 儲存/開啟為 `.diagram.json`,匯出為 PNG 或 SVG - - Undo/redo、對齊、分佈、Grid、Snap、Zoom 控制 -- **檔案樹右鍵選單** — 在專案檔案樹中對任何檔案或資料夾按右鍵,可建立檔案/資料夾、重新命名、刪除、複製絕對或相對路徑、在系統檔案管理器中開啟。重新命名或刪除目前已開在編輯器分頁中的檔案時,分頁會同步更新。 -- **套件管理器** — 直接從 IDE 選單安裝自動化模組和建構工具,無需離開編輯器。 -- **整合文件** — 從選單列快速存取每個自動化模組的文件和 GitHub 頁面。 +![AI 程式碼審查用戶端](../images/ai_code_review.png) -### AI 輔助開發 +*畫面為送出前的狀態。* 把選取的程式碼送到 LLM 端點,再接受或拒絕建議——統計會記錄在 `~/.pybreeze/response_stats.txt`。URL 會經過 SSRF 驗證,連線只會連到檢查過的位址,不跟隨重新導向,回應本文在送進面板前也有大小上限。 -- **AI 程式碼審查** — 將程式碼傳送到 LLM API 端點進行自動化程式碼審查。可直接在 IDE 中接受或拒絕建議。 -- **思維鏈程式碼審查(prthinker)** — 以 [prthinker](https://github.com/JE-Chen/Code-Review-Framework-Combining-Large-Language-Models-and-Chain-of-Thought-Reasoning) 的審查流程審查正在編輯的檔案,或審查一個 Pull Request,輸出即時流進執行視窗。一張設定表填完推論後端(審查伺服器、OpenAI 相容端點、Anthropic 或本機模型)、程式碼託管平台與儲存庫;金鑰與權杖以環境變數交給審查,不會出現在命令列上——那是工作管理員看得到的地方。 -- **CoT(思維鏈)提示詞編輯器** — 建立和管理多步驟 CoT 提示詞,用於結構化程式碼分析,包含: - - 程式碼審查提示詞 - - Code Smell 偵測 - - 程式碼檢查分析 - - 逐步分析 - - 摘要生成 -- **Skill 提示詞編輯器** — 定義和管理可重複使用的技能型提示詞(程式碼解說、程式碼審查範本),可傳送至 LLM API。 -- **Skill Send GUI** — 在獨立的分頁或停靠面板中選擇技能提示詞範本、視需要編輯提示詞內容、傳送到 LLM API 端點並檢視回應。 +### 思維鏈程式碼審查(prthinker) -### 插件系統 +對正在編輯的檔案或一個 Pull Request 執行 [prthinker](https://github.com/JE-Chen/Code-Review-Framework-Combining-Large-Language-Models-and-Chain-of-Thought-Reasoning) 審查流程,輸出即時傳進執行視窗。 -PyBreeze 支援可擴展的插件架構,用於: +![prthinker 設定](../images/prthinker_setting.png) -- **語法高亮** — 透過插件為任何程式語言新增語法高亮 -- **UI 翻譯** — 透過翻譯插件新增新的介面語言 -- **執行設定** — 為編譯式和直譯式語言新增「以...執行」支援(C、C++、Go、Java、Rust 等) -- **插件瀏覽器** — 直接在 IDE 中從遠端儲存庫瀏覽並安裝插件 +一張設定表就包含推論後端(`remote`、`local`、OpenAI 相容、Anthropic、Gemini、Cohere、Mistral、`claude-cli`、`codex-cli`)、程式碼託管平台(GitHub/GitLab/Gitea)與儲存庫。**金鑰與權杖以環境變數交給審查,絕不放在命令列上**——那裡會被行程清單看到——而且在日誌中會被遮蔽。模型名稱會交給所選的後端。規則檢索(RAG)預設為 `off`,設為 `remote` 時會向 prthinker 伺服器的 `/rag` 查詢:prthinker 的本機規則索引隨它的儲存庫提供,不在由它安裝的套件裡。審查以 `Python Env` 選定的直譯器執行,因此 PyBreeze 本身可以停留在比 prthinker 所需的 3.12 更舊的 Python 上。 -插件會從 `jeditor_plugins/` 目錄自動探索載入。完整文件請參閱 [PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md)。 +### CoT 提示詞編輯器 -**內建插件:** C、C++、Go、Java、Rust 語法高亮與執行支援;法文翻譯。 +![CoT 提示詞編輯器](../images/cot_prompt_editor.png) -### 多語言介面 +建立與管理多步驟的審查鏈:初次摘要 → 初次程式碼審查 → 評審該次審查 → linter → 程式碼異味偵測 → 逐步分析 → 總結 → 評審總結。每一步都會引用它所需的前面步驟的答案。檔案受到監看,所以外部的修改會立即反映出來。 -IDE 介面支援多種語言: +從 **Tools → AI → CoT Code Review** 執行這條審查鏈(以分頁開啟,或從 Dock 選單以停駐面板開啟):貼上程式碼、填入端點 URL,每一步的答案一到就會出現在選擇器中。 + +### Skill 提示詞編輯器與 Skill Send + +| Skill 提示詞編輯器 | Skill Send | +|---|---| +| ![Skill 提示詞編輯器](../images/skill_prompt_editor.png) | ![Skill Send](../images/skills_send.png) | + +定義可重複使用的 skill 提示詞(程式碼解說、程式碼審查),再從專用的分頁或停駐面板選一個、視需要編輯,然後送到 LLM 端點。*兩者都是送出前的狀態——拍攝這些截圖時沒有連線到任何端點。* + +--- + +## 插件系統 + +PyBreeze 沿用 JEditor 的插件架構,會自動從工作目錄中的 `jeditor_plugins/` 目錄探索插件。插件可以註冊: + +- **語法高亮** — 任何語言的關鍵字集與規則 +- **介面翻譯** — 新的介面語言 +- **執行設定** — 為直譯式(`go run main.go`)與編譯式(`gcc main.c -o main` 後執行)語言提供「Run with…」,透過 PyBreeze 的 `FileRunnerProcess` 執行,並在結束後清掉編譯產物 +- **插件瀏覽器** — 在 IDE 內瀏覽並安裝遠端儲存庫中的插件 + +已載入的插件會出現在它們專屬的 **Plugins** 選單下,附一個 About 項目和一個執行動作,動作名稱標示它能執行的副檔名。[PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md) 說明 PyBreeze 額外提供的部分,並連到 JEditor 的指南,那裡有完整的 API 與實作範例(C、C++、Go、Java、Rust,以及法文翻譯)。 + +--- + +## 多語言介面 - **English**(英文,預設) -- **繁體中文** -- 可透過插件新增其他語言 +- **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 731 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯插件加入。 --- -## 架構設計 +## 架構 ```mermaid flowchart TB UI["PyBreeze UI · PySide6"] - subgraph Editor["JEditor 基礎編輯器"] + subgraph Editor["JEditor (Base Editor)"] direction LR - E1["程式碼編輯器 + 分頁"] - E2["檔案樹"] - E3["語法高亮"] - E4["插件系統"] + E1["Code Editor + Tabs"] + E2["File Tree"] + E3["Syntax Highlighting"] + E4["Plugin System"] end - subgraph Automation["自動化選單"] + subgraph Automation["Automation Menu"] direction LR A1["APITestka"] A2["AutoControl"] @@ -142,7 +240,7 @@ flowchart TB A7["TestPioneer"] end - subgraph Executors["子行程執行器 · TaskProcessManager"] + subgraph Executors["Subprocess Executors · TaskProcessManager"] direction LR X1["je_api_testka"] X2["je_auto_control"] @@ -153,21 +251,20 @@ flowchart TB X7["test_pioneer"] end - subgraph Tools["工具"] + subgraph Tools["Tools"] direction LR T1["SSH · paramiko"] - T2["AI 程式碼審查"] - T3["CoT 提示詞編輯器"] - T4["Skill 提示詞編輯器"] - T5["Skill Send GUI"] - T6["架構圖編輯器"] - T7["JupyterLab"] + T2["AI Code Review"] + T3["Prompt Editors"] + T4["Diagram Editor"] + T5["HTTP Toolbelt"] + T6["JupyterLab"] end - subgraph Install["安裝選單"] + subgraph Install["Install Menu"] direction LR - I1["模組安裝器"] - I2["建構工具"] + I1["Module Installers"] + I2["Build Tools"] end UI --> Editor @@ -184,41 +281,13 @@ flowchart TB A7 --> X7 ``` -PyBreeze 採用模組化架構: - -``` -PyBreeze UI (PySide6) -├── JEditor(基礎編輯器引擎) -│ ├── 程式碼編輯器與分頁 -│ ├── 檔案樹瀏覽 -│ ├── 語法高亮引擎 -│ └── 插件系統 -├── 自動化選單 -│ ├── APITestka ──→ APITestka 執行器 ──→ je_api_testka -│ ├── AutoControl ──→ AutoControl 執行器 ──→ je_auto_control -│ ├── WebRunner ──→ WebRunner 執行器 ──→ je_web_runner -│ ├── LoadDensity ──→ LoadDensity 執行器 ──→ je_load_density -│ ├── FileAutomation ──→ FileAutomation 執行器 ──→ automation-file -│ ├── MailThunder ──→ MailThunder 執行器 ──→ je-mail-thunder -│ └── TestPioneer ──→ TestPioneer 執行器 ──→ test_pioneer -├── 工具 -│ ├── SSH 用戶端(paramiko) -│ ├── AI 程式碼審查用戶端 -│ ├── CoT 提示詞編輯器 -│ ├── Skill 提示詞編輯器 -│ ├── Skill Send GUI -│ ├── 架構圖編輯器(WYSIWYG、Mermaid 匯入、PNG/SVG 匯出) -│ └── JupyterLab 整合 -└── 安裝選單 - ├── 自動化模組安裝器 - └── 建構工具安裝器 -``` +**編輯器行程從不執行你的腳本。** 每個自動化模組都以 `python -m ` 在專案的直譯器中啟動,並使用 `shell=False`。兩條常駐執行緒把 stdout 與 stderr 讀進執行緒安全的佇列;一個 100 ms 的 `QTimer` 以有上限的批次把它們取出,送到 UI 執行緒。腳本當掉、卡住或陷入無限輸出迴圈,都不會連帶拖垮 IDE。 -每個自動化模組都透過 `PythonTaskProcessManager` 在獨立的子行程中執行,提供行程隔離,防止崩潰影響 IDE。 +逐一介紹各模組的程式碼導覽,請參閱 [architecture_explore.md](../architecture_explore.md)。 --- -## 安裝方式 +## 安裝 ### 從 PyPI 安裝 @@ -236,103 +305,48 @@ pip install -r requirements.txt ### 系統需求 -- **Python**:3.10 或更高版本 +- **Python**:3.10 – 3.14 - **作業系統**:Windows、macOS、Linux -- **GUI 框架**:PySide6 6.11.2(自動安裝) +- **GUI**:PySide6 6.11.2(自動安裝) --- ## 快速開始 -### 透過命令列執行 - ```bash -python -m pybreeze +python -m pybreeze # 命令列 +python exe/start_pybreeze.py # 從 exe 目錄 ``` -### 透過 Python 腳本執行 - ```python from pybreeze import start_editor -start_editor() +start_editor() # 預設 dark_amber 主題 +start_editor(theme="dark_teal.xml") # 任何 qt_material 主題 ``` -### 從 exe 目錄執行 +啟動後: -```bash -python exe/start_pybreeze.py -``` - -啟動後,您可以: - -1. **撰寫自動化腳本** — 在編輯器中享有語法感知的自動補全 -2. **執行腳本** — 透過 `自動化` 選單,選擇目標模組(APITestka、WebRunner 等) -3. **檢視結果** — 在整合式輸出面板中查看 -4. **生成報告** — 支援 HTML/JSON/XML 格式 -5. **寄送報告** — 使用 MailThunder 整合功能透過電子郵件發送 +1. **撰寫** — 在編輯器中撰寫自動化腳本,自動化關鍵字會隨著輸入高亮 +2. **執行** — 從 `Automation` 選單執行,選擇目標模組 +3. **觀看** — 輸出即時傳進執行視窗 +4. **產生** — HTML/JSON/XML 報告 +5. **寄送** — 透過 MailThunder 整合以電子郵件寄出 --- -## 整合自動化模組 - -### APITestka — API 測試 - -- HTTP 方法測試(GET、POST、PUT、DELETE 等) -- 透過 httpx 支援非同步 HTTP -- 使用 Flask 建立 Mock 伺服器 -- 報告生成(HTML、JSON、XML) -- 基於排程器的事件觸發 -- Socket 伺服器支援 +## 整合的自動化模組 -### AutoControl — GUI 自動化 - -- 滑鼠控制(點擊、拖曳、滾動、位置追蹤) -- 鍵盤模擬(輸入、快捷鍵、按鍵按下/釋放) -- 圖像辨識與定位點擊 -- 螢幕截圖 -- 動作錄製與重播 -- Shell 指令執行 -- 行程管理 - -### WebRunner — Web 自動化 - -- 瀏覽器驅動整合 -- 元素定位與互動 -- 基於 Web 的測試腳本 -- 報告生成 - -### LoadDensity — 負載測試 - -- 併發請求模擬 -- 效能指標收集 -- 壓力測試情境管理 -- 報告生成 - -### MailThunder — 郵件自動化 - -- SMTP 郵件寄送 -- HTML 報告傳遞 -- 附件支援 -- 基於環境變數的設定 - -### TestPioneer — 測試框架 - -- 基於 YAML 的測試定義 -- 範本生成 -- 結構化測試執行 - -### File Automation — 檔案自動化 - -- 自動化檔案與目錄操作 -- 批次檔案處理 - -### prthinker — 思維鏈程式碼審查 - -- 審查正在編輯的檔案,或審查 GitHub / GitLab / Gitea 上的 Pull Request -- 推論後端可選:審查伺服器、OpenAI 相容端點、Anthropic,或本機模型 -- 設定存放在使用者目錄的 `~/.pybreeze/prthinker_setting.json`;金鑰以環境變數交給審查 -- 從它自己的原始碼資料夾安裝(**安裝 ▸ 自動化 ▸ 安裝 prthinker**),需要 Python 3.12 以上 +| 模組 | 功能 | +|---|---| +| **APITestka** | HTTP 方法、透過 httpx 的非同步請求、Flask Mock 伺服器、HTML/JSON/XML 報告、排程觸發、socket 伺服器、JSON-schema 與 JSONPath 斷言、SLA 檢查、錄製重播 cassette | +| **AutoControl** | 滑鼠(點擊、拖曳、捲動、位置)、鍵盤(輸入、快捷鍵、按下/放開)、圖像辨識與定位點擊、螢幕截圖、錄製與播放、shell 與行程控制 | +| **WebRunner** | 瀏覽器驅動程式整合、元素定位與互動、Web 測試腳本、報告 | +| **LoadDensity** | 併發請求模擬、效能指標、壓力情境管理、報告 | +| **MailThunder** | SMTP 寄信、HTML 報告寄送、附件、以環境變數設定 | +| **TestPioneer** | YAML 測試定義、範本產生、結構化執行 | +| **File Automation** | 自動化檔案與目錄操作、批次處理 | +| **prthinker** | 對檔案或 Pull Request 進行思維鏈程式碼審查;設定存於 `~/.pybreeze/prthinker_setting.json`;透過 `Install ▸ Automation ▸ Install prthinker` 從它自己的原始碼資料夾安裝(需要 Python 3.12 以上) | --- @@ -341,78 +355,97 @@ python exe/start_pybreeze.py ``` PyBreeze/ ├── pybreeze/ -│ ├── __init__.py # 公開 API(start_editor、插件 re-export) -│ ├── __main__.py # 進入點(python -m pybreeze) +│ ├── __init__.py # 公開 API(start_editor、插件 re-export) +│ ├── __main__.py # 進入點(python -m pybreeze) │ ├── extend/ -│ │ ├── mail_thunder_extend/ # 測試後郵件報告寄送 -│ │ ├── process_executor/ # 各自動化模組的子行程管理器 -│ │ │ ├── api_testka/ -│ │ │ ├── auto_control/ -│ │ │ ├── file_automation/ -│ │ │ ├── load_density/ -│ │ │ ├── mail_thunder/ -│ │ │ ├── test_pioneer/ -│ │ │ └── web_runner/ -│ │ └── process_executor/python_task_process_manager.py -│ ├── extend_multi_language/ # 內建翻譯(英文、繁體中文) +│ │ ├── process_executor/ # 子行程隔離層 +│ │ │ ├── python_task_process_manager.py # TaskProcessManager(核心) +│ │ │ ├── process_executor_utils.py # build_process / start_process +│ │ │ ├── file_runner_process.py # 插件執行設定(任何語言) +│ │ │ ├── queue_pump.py # 共用的管線讀取器 + QTimer 取出 +│ │ │ ├── api_testka/ auto_control/ web_runner/ +│ │ │ ├── load_density/ file_automation/ mail_thunder/ +│ │ │ ├── test_pioneer/ prthinker/ +│ │ ├── mail_thunder_extend/ # 測試後郵件報告掛鉤 +│ │ └── prthinker_extend/ # prthinker 設定與參數組裝 +│ ├── extend_multi_language/ # 內建多語言(英文、繁體中文) │ ├── pybreeze_ui/ -│ │ ├── editor_main/ # 主視窗(擴展 JEditor)+ 檔案樹右鍵選單 -│ │ ├── connect_gui/ssh/ # SSH 用戶端元件(TOFU host key 驗證) -│ │ ├── diagram_editor/ # WYSIWYG 架構圖編輯器 -│ │ ├── extend_ai_gui/ # AI 程式碼審查與提示詞編輯器 -│ │ ├── jupyter_lab_gui/ # JupyterLab 整合 -│ │ ├── menu/ # 選單列建構 -│ │ ├── syntax/ # 自動化關鍵字定義 -│ │ └── show_code_window/ # 程式碼顯示元件 -│ └── utils/ # 日誌、例外處理、檔案處理、套件管理 -├── exe/ # 獨立啟動器與建構設定 -├── docs/ # Sphinx 文件原始碼 -├── test/ # 單元測試 -├── images/ # 截圖 -├── architecture_diagram/ # 架構圖 -├── PLUGIN_GUIDE.md # 插件開發文件 -├── pyproject.toml # 套件設定 -├── requirements.txt # 執行階段依賴項 -└── dev_requirements.txt # 開發依賴項 +│ │ ├── editor_main/ # 主視窗 + 檔案樹右鍵選單 +│ │ ├── menu/ # Automation / Install / Tools / 插件選單 +│ │ ├── tools_gui/ # cURL、HAR、JWT、diff、regex 等工具分頁 +│ │ ├── diagram_editor/ # 所見即所得圖表編輯器 +│ │ ├── extend_ai_gui/ # CoT 審查、提示詞編輯器、skill send +│ │ ├── connect_gui/ # SSH 終端機 + SFTP 檔案樹、AI 審查用戶端 +│ │ ├── jupyter_lab_gui/ # JupyterLab 分頁 +│ │ ├── show_code_window/ # CodeWindow(執行輸出) +│ │ ├── dialog/ # prthinker 設定對話框 +│ │ └── syntax/ # 自動化關鍵字定義 +│ └── utils/ # curl/HAR 解析、標頭、JWT、雜湊、 +│ # URL 驗證、日誌、例外…… +├── exe/ # 獨立啟動器與建置設定 +├── docs/ # Sphinx 文件原始碼 +├── test/ # 單元測試(test_utils)+ 啟動測試 +├── images/ # 截圖 +├── architecture_explore.md # 逐模組的架構筆記 +├── PLUGIN_GUIDE.md # 插件開發文件 +├── pyproject.toml # 套件設定(穩定版) +├── dev.toml # 套件設定(開發通道) +└── requirements.txt # 執行階段相依套件 ``` --- -## 依賴項目 +## 相依套件 ### 執行階段 | 套件 | 用途 | |---|---| -| `PySide6` (6.11.2) | GUI 框架(Qt for Python)| +| `PySide6` (6.11.2) | GUI 框架(Qt for Python) | | `je-editor` | 基礎程式碼編輯器引擎 | | `je_api_testka` | API 測試自動化 | -| `je_auto_control` | GUI/桌面自動化 | +| `je_auto_control` | GUI/桌面自動化 | | `je_web_runner` | Web 瀏覽器自動化 | | `je_load_density` | 負載與壓力測試 | | `je-mail-thunder` | 郵件自動化 | | `automation-file` | 檔案操作自動化 | -| `test_pioneer` | 基於 YAML 的測試框架 | +| `test_pioneer` | 以 YAML 為基礎的測試框架 | | `paramiko` | SSH 用戶端支援 | -| `jupyterlab` | 整合式筆記本環境 | +| `jupyterlab` | 整合的筆記本環境 | ### 開發 -`build`、`twine`、`sphinx`、`sphinx-rtd-theme`、`auto-py-to-exe` +`build`、`twine`、`sphinx`、`sphinx-rtd-theme`、`auto-py-to-exe`、`pytest`、`pytest-cov`、`hypothesis`、`ruff` + +--- + +## 測試與 CI + +```bash +python -m pip install -r dev_requirements.txt +python -m pytest test/test_utils/ -v --tb=short +``` + +- **單元測試** — `test/test_utils/`,涵蓋純邏輯層(curl 與 HAR 解析、標頭分析、SSRF 驗證、JWT、雜湊、時間戳記、比對),加上透過 `QT_QPA_PLATFORM=offscreen` 的無視窗 Qt 元件測試,以及針對各解析器的 Hypothesis 性質測試 +- **啟動測試** — `test/unit_test/start_automation/` 以 debug 模式啟動 IDE,確認它能正常開啟並乾淨地結束 +- **CI** — 在 Windows 上以 GitHub Actions 跑 Python 3.10 – 3.14,每次 push 與 PR 都會執行,另外每晚執行一次 +- **靜態分析** — SonarCloud、Codacy 與 Bandit --- ## 目標使用者 -- **Python 開發者** — 一個輕量、專用的環境,用於構建自動化腳本,無需承受重量級通用 IDE 的負擔 -- **SDET(測試開發工程師)** — 需要在同一工具中同時維護 Web、API 和效能測試的專業人士 -- **自動化初學者** — 一個友善的 IDE,透過零設定環境降低 Python 自動化的入門門檻 -- **DevOps 團隊** — 一個在 CI/CD 流水線中快速構建和除錯整合測試套件的平台 +- **Python 開發者** — 一個輕量、專用的自動化腳本環境,沒有通用 IDE 的額外負擔 +- **SDET(測試開發工程師)** — 用同一個工具並行維護 Web、API 與效能測試 +- **自動化初學者** — 零設定的環境建置,每個模組都有選單 +- **DevOps 團隊** — 建置與除錯要送進 CI/CD 的整合測試套件的地方 --- ## 授權條款 -本專案採用 MIT 授權條款——詳情請參閱 [LICENSE](../LICENSE) 檔案。 +MIT — 請參閱 [LICENSE](../LICENSE)。Copyright (c) 2022 JE-Chen + +--- -Copyright (c) 2022 JE-Chen +截圖是在 Windows 11 上以預設的 `dark_amber` 主題,從實際的 PyBreeze 元件渲染而來;每個工具中的範例資料都是由實際程式路徑處理過的真實輸入。 diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 15109e2b..aa27e70b 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -676,3 +676,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 731 keys in each language. 2265 tests in 125 files. `ruff check` clean. - **Files**: `pybreeze/extend/process_executor/queue_pump.py`, `pybreeze/extend/process_executor/file_runner_process.py`, `pybreeze/extend/process_executor/python_task_process_manager.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_run_notice.py`, `README.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-275 · 2026-09-24 · The Traditional and Simplified Chinese READMEs follow README.md again · #docs #readme + +- **What**: `README/README_zh-TW.md` and `README/README_zh-CN.md` still had an old layout: no screenshot tour, the built-in tools in one list instead of a section each (cURL, HAR, Response Inspector, Header Analyzer, Diff, the everyday utilities, Diagram Editor, SSH), no Testing & CI section, and none of what README.md has said since about the run window's Stop button, SFTP transfer cancelling, the key file browser, the regex tab's Enter, the translated tool errors and run-window notices or the key count. CLAUDE.md now requires README.md and every translation to be aligned in structure and content. +- **Change**: both are translations of README.md as it stands, section by section: the same headings in the same order (16 second-level, 19 third-level), tables, lists, code blocks, screenshots and captions; relative links and images carry `../`; the tables of contents point at the translated headings. zh-TW uses Taiwan usage and names the menus as the Traditional Chinese interface shows them (自動化, 工具, 安裝) beside the English in the screenshots; code, commands, file names and the Mermaid example stay as in README.md. +- **Checks**: every relative link and image in both files resolves; all three give the same key count (731); no mainland term in zh-TW. +- **Files**: `README/README_zh-TW.md`, `README/README_zh-CN.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b271fd8b..24f57f95 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-275 | 2026-09-24 | The Traditional and Simplified Chinese READMEs follow README.md again | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-274 | 2026-09-24 | The exit-code line and the held-output note in the IDE language too | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | | U-20260924-273 | 2026-09-24 | Why a report mail was not sent, in the IDE language | #fix #i18n #mail | [2026-09-b](2026-09-b.md) | | U-20260924-272 | 2026-09-24 | Refactor: the reasons a report mail was not sent come from exception_tags | #refactor #i18n #mail | [2026-09-b](2026-09-b.md) | @@ -356,4 +357,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 72 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 73 | From 2bb95cbfd0b43557df6975e614ec5673e31a8ff0 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 17:02:30 +0800 Subject: [PATCH 006/166] Check a trimmed diff against the plain one only on small texts --- architecture_explore.md | 4 ++-- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- pybreeze/utils/diff_tools/text_diff.py | 10 ++++++++-- test/test_utils/test_text_diff.py | 23 +++++++++++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index fe92872f..4345e10f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -227,7 +227,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) | `UrlBuilderGUI` | `utils/url_tools/` | URL 拆成 JSON 元件 / 由元件組回 URL | | `RegexGUI` | `utils/regex_tools/` | regex 測試,flag 勾選、列出每個 match 與群組。pattern 在另一個行程裡跑(`find_matches_bounded()`:從原始碼執行時是 `python -I -S -c` 跑一段只用標準函式庫的固定腳本,工作用 JSON 從 stdin 進、結果從 stdout 出,5 秒後 kill;打包版沒有直譯器可用,仍是 multiprocessing spawn),分頁用 `RegexMatchThread` 等它:`re` 開始比對後就停不下來,災難性回溯只有整個行程能停。spawn 會重新匯入啟動 IDE 的腳本,README 那種沒有 `__main__` 防護的腳本會每跑一次就再開一個 IDE。還在跑的 worker 記在 `_RUNNING`,分頁關閉時 `stop_running_workers()` 結束它(IDE 以 `os._exit` 結束,子行程不會跟著走);執行中不能存檔,列到 `MAX_MATCHES` 上限時會註明可能還有更多 | | `HttpStatusGUI` | `utils/http_reference/` | 狀態碼參考,可依碼前綴或描述搜尋 | -| `DiffGUI` | `utils/diff_tools/` | unified diff + 增刪統計,`compare_texts()` 的統計與 diff 共用同一次比對(`_TrimmedMatcher`:先把相同的開頭結尾放一邊再比對中間,長而重複的文字改一行就是一行;放一邊有時反而比對得更差(`b b a b a` 對 `b a c b`),所以有放一邊時 `_closest_match` 也照原樣比一次,取改動行數少的;autojunk 照 difflib 的預設,關掉的話重複的文字比對時間隨行數平方成長;diff 照 `difflib.unified_diff` 的格式從它的 grouped opcodes 寫出),在 `DiffThread` 上算、不佔 UI 執行緒(4 萬行要四秒多),比對中按鈕停用、關閉時交給 `let_run_out()`。逐行比不出差別、文字卻不同時(最後少一個換行、`\r\n` 對 `\n`),改成連行尾一起比:少換行的那行下面標 `\ No newline at end of file`,其他行尾寫出來 | +| `DiffGUI` | `utils/diff_tools/` | unified diff + 增刪統計,`compare_texts()` 的統計與 diff 共用同一次比對(`_TrimmedMatcher`:先把相同的開頭結尾放一邊再比對中間,長而重複的文字改一行就是一行;放一邊有時反而比對得更差(`b b a b a` 對 `b a c b`),所以有放一邊、且兩段合計不超過 2,000 行時,`_closest_match` 也照原樣比一次,取改動行數少的(更大的文字再比一次會讓等待加倍);autojunk 照 difflib 的預設,關掉的話重複的文字比對時間隨行數平方成長;diff 照 `difflib.unified_diff` 的格式從它的 grouped opcodes 寫出),在 `DiffThread` 上算、不佔 UI 執行緒(4 萬行要四秒多),比對中按鈕停用、關閉時交給 `let_run_out()`。逐行比不出差別、文字卻不同時(最後少一個換行、`\r\n` 對 `\n`),改成連行尾一起比:少換行的那行下面標 `\ No newline at end of file`,其他行尾寫出來 | | `JsonFormatGUI` | `utils/json_format/` | 美化 / 壓縮 / 驗證 | | `HeaderAnalyzerGUI` | `utils/header_tools/` | HTTP header 安全稽核(HSTS、CSP、CORS、Set-Cookie、banner…)| | `ResponseInspectorGUI` | `utils/response_inspector/` | 貼整包 response → 拆狀態列/headers/body,順便挖出 JWT;`curl -i` 印出的多段回應(`100 Continue`、proxy 的 `Connection established`、`-L` 的轉址)取最後一段;只有一行又沒有狀態列就當 body | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2265 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2267 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index aa27e70b..7080a53a 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -684,3 +684,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Checks**: every relative link and image in both files resolves; all three give the same key count (731); no mainland term in zh-TW. - **Files**: `README/README_zh-TW.md`, `README/README_zh-CN.md` - **Open items**: none. + +## U-20260924-276 · 2026-09-24 · Check the trimmed diff against the plain one only on small texts · #fix #diff #perf + +- **What**: U-20260924-264 made `compare_texts` match a second time, plainly, whenever an unchanged head or tail had been set aside, and keep the diff that changes fewer lines. On the Diff tab's own large case (20,000 lines on each side, every tenth changed) that took the comparison from 6.4 s to 16.6 s for the same 2,000-line diff, and `test_diff_gui.py`'s large-comparison test ran out of time under coverage. Found by a coverage run. +- **Fix**: the plain match is made only when both texts together have at most `_PLAIN_MATCH_MAX_LINES` (2,000) lines, where it takes milliseconds; the case trimming gets wrong is a small, ambiguous one (`b b a b a` against `b a c b`). The large case is back to about 7 s. +- **Tests**: `test_text_diff.py` +2 (a 21-line text with a common head is matched plainly too; a 3,002-line one is not), and the property that the diff is never longer than difflib's, on its small texts, still passes. +- **Result / numbers**: 2267 tests in 125 files. `ruff check` clean. +- **Files**: `pybreeze/utils/diff_tools/text_diff.py`, `test/test_utils/test_text_diff.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 24f57f95..5caac33a 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-276 | 2026-09-24 | Check the trimmed diff against the plain one only on small texts | #fix #diff #perf | [2026-09-b](2026-09-b.md) | | U-20260924-275 | 2026-09-24 | The Traditional and Simplified Chinese READMEs follow README.md again | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-274 | 2026-09-24 | The exit-code line and the held-output note in the IDE language too | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | | U-20260924-273 | 2026-09-24 | Why a report mail was not sent, in the IDE language | #fix #i18n #mail | [2026-09-b](2026-09-b.md) | @@ -357,4 +358,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 73 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 74 | diff --git a/pybreeze/utils/diff_tools/text_diff.py b/pybreeze/utils/diff_tools/text_diff.py index 9d381d09..da83cc55 100644 --- a/pybreeze/utils/diff_tools/text_diff.py +++ b/pybreeze/utils/diff_tools/text_diff.py @@ -16,6 +16,9 @@ _CONTEXT_LINES = 3 # The "--- left" and "+++ right" lines a non-empty unified diff starts with _HEADER_LINES = 2 +# Most lines, both texts together, for which a trimmed match is checked +# against the plain one (``_closest_match``); matching takes milliseconds there +_PLAIN_MATCH_MAX_LINES = 2000 # diff's own note under a last line that has no newline _NO_NEWLINE_MARK = "\\ No newline at end of file" @@ -157,11 +160,14 @@ def _closest_match(left_lines: list[str], right_lines: list[str]) -> tuple[diffl finds the smaller diff, but not always: in ``b b a b a`` against ``b a c b`` the first ``b`` taken as unchanged left a worse match, five lines changed where ``difflib`` changes three. When anything was set - aside, the plain match is made too and the smaller one kept. + aside, the plain match is made too and the smaller one kept -- for texts + of up to ``_PLAIN_MATCH_MAX_LINES`` lines between them. On larger ones a + second match doubled a wait of seconds (40,000 lines: 6 s became 16 s) + for the same diff. """ trimmed = _TrimmedMatcher(left_lines, right_lines) best = (trimmed, *_changed_lines(trimmed)) - if trimmed.trimmed_any(): + if trimmed.trimmed_any() and len(left_lines) + len(right_lines) <= _PLAIN_MATCH_MAX_LINES: plain = difflib.SequenceMatcher(None, left_lines, right_lines) added, removed = _changed_lines(plain) if added + removed < best[1] + best[2]: diff --git a/test/test_utils/test_text_diff.py b/test/test_utils/test_text_diff.py index 776d2301..a4fab36f 100644 --- a/test/test_utils/test_text_diff.py +++ b/test/test_utils/test_text_diff.py @@ -223,3 +223,26 @@ def test_repetitive_text_just_under_2000_lines_is_quick(): compare_texts(left, right) assert time.monotonic() - started < 5 + + +@pytest.mark.parametrize(("lines", "plain_matches"), [(10, 1), (1500, 0)]) +def test_the_plain_match_is_made_only_for_small_texts(monkeypatch, lines, plain_matches): + # Made for every text, it took a 40,000-line comparison from 6 s to 16 s + import difflib + + from pybreeze.utils.diff_tools import text_diff + + made: list = [] + + class Counting(difflib.SequenceMatcher): + def __init__(self, *args, **kwargs): + made.append(True) + super().__init__(*args, **kwargs) + + monkeypatch.setattr(text_diff.difflib, "SequenceMatcher", Counting) + left = "\n".join(["same"] + [f"l{i}" for i in range(lines)]) + right = "\n".join(["same"] + [f"r{i}" for i in range(lines)]) + + text_diff.compare_texts(left, right) + + assert len(made) == plain_matches From 312634ee677e6607900e8d65ab4569d035a9bc29 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 17:06:58 +0800 Subject: [PATCH 007/166] Test the diagram editor's align and distribute --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 8 ++ docs/updates/README.md | 3 +- test/test_utils/test_diagram_align.py | 107 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_diagram_align.py diff --git a/architecture_explore.md b/architecture_explore.md index 4345e10f..a70822d0 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 125 個 `test_*.py`、2267 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 126 個 `test_*.py`、2278 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 7080a53a..763fa3d7 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -693,3 +693,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2267 tests in 125 files. `ruff check` clean. - **Files**: `pybreeze/utils/diff_tools/text_diff.py`, `test/test_utils/test_text_diff.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-277 · 2026-09-24 · Tests for the diagram editor's align and distribute · #test #diagram + +- **What**: a coverage run (87% over `pybreeze/`) showed the diagram scene's eight align and distribute operations (`align_left` ... `distribute_v`) run by no test. +- **Change**: `test_diagram_align.py` (11): each edge lines up where the outermost node's edge was; centres line up on their average; distributing keeps the outer two in place and leaves equal gaps; one undo takes a whole alignment back and leaves nothing more to undo; with too few nodes selected nothing moves and no undo step is recorded. They pass on the code as it is. +- **Result / numbers**: 2278 tests in 126 files. +- **Files**: `test/test_utils/test_diagram_align.py` (new), `architecture_explore.md` §18 +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 5caac33a..229bbdb3 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-277 | 2026-09-24 | Tests for the diagram editor's align and distribute | #test #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-276 | 2026-09-24 | Check the trimmed diff against the plain one only on small texts | #fix #diff #perf | [2026-09-b](2026-09-b.md) | | U-20260924-275 | 2026-09-24 | The Traditional and Simplified Chinese READMEs follow README.md again | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-274 | 2026-09-24 | The exit-code line and the held-output note in the IDE language too | #fix #i18n #run-window | [2026-09-b](2026-09-b.md) | @@ -358,4 +359,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 74 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 75 | diff --git a/test/test_utils/test_diagram_align.py b/test/test_utils/test_diagram_align.py new file mode 100644 index 00000000..b36a32a0 --- /dev/null +++ b/test/test_utils/test_diagram_align.py @@ -0,0 +1,107 @@ +"""Align and distribute in the diagram editor: where the selected nodes end up, and one undo step back.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.diagram_editor.diagram_items import DiagramNode +from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +@pytest.fixture() +def scene(app): + diagram = DiagramScene() + yield diagram + diagram.deleteLater() + + +def _nodes(scene: DiagramScene, *boxes: tuple[float, float, float, float]) -> list[DiagramNode]: + """Selected nodes at (x, y, w, h).""" + nodes = [] + for x, y, w, h in boxes: + node = DiagramNode(x=x, y=y, w=w, h=h, text="n") + scene.addItem(node) + node.setSelected(True) + nodes.append(node) + return nodes + + +def _positions(nodes: list[DiagramNode]) -> list[tuple[float, float]]: + return [(node.pos().x(), node.pos().y()) for node in nodes] + + +_THREE = ((10, 100, 100, 60), (200, 40, 50, 30), (400, 0, 80, 120)) + + +@pytest.mark.parametrize(("operation", "expected"), [ + ("align_left", [(10, 100), (10, 40), (10, 0)]), + ("align_right", [(380, 100), (430, 40), (400, 0)]), + ("align_top", [(10, 0), (200, 0), (400, 0)]), + ("align_bottom", [(10, 100), (200, 130), (400, 40)]), +]) +def test_each_edge_lines_up(scene, operation, expected): + nodes = _nodes(scene, *_THREE) + + getattr(scene, operation)() + + assert _positions(nodes) == expected + + +def test_centres_line_up_on_their_average(scene): + nodes = _nodes(scene, (0, 0, 100, 40), (300, 100, 50, 20)) + + scene.align_center_h() + scene.align_center_v() + + centres = {(node.pos().x() + node.node_w / 2, node.pos().y() + node.node_h / 2) for node in nodes} + assert centres == {(187.5, 65.0)} + + +def test_distributing_leaves_equal_gaps_between_the_outer_two(scene): + nodes = _nodes(scene, (0, 0, 100, 40), (130, 10, 60, 40), (400, 20, 100, 40)) + + scene.distribute_h() + + lefts = [node.pos().x() for node in nodes] + gaps = [lefts[1] - (lefts[0] + 100), lefts[2] - (lefts[1] + 60)] + assert lefts[0] == 0 and lefts[2] == 400 + assert gaps[0] == pytest.approx(gaps[1]) + + +def test_one_undo_takes_the_whole_alignment_back(scene): + nodes = _nodes(scene, *_THREE) + before = _positions(nodes) + + scene.align_left() + scene.undo_stack.undo() + + # Undo rebuilds the items from a snapshot: compare what is on the canvas now + now = sorted((item.pos().x(), item.pos().y()) for item in scene.items() if isinstance(item, DiagramNode)) + assert now == sorted(before) + assert not scene.undo_stack.canUndo() + + +@pytest.mark.parametrize("operation", ["align_left", "align_center_v", "distribute_h", "distribute_v"]) +def test_too_few_nodes_changes_nothing_and_leaves_no_undo_step(scene, operation): + nodes = _nodes(scene, (0, 0, 100, 40), (300, 100, 50, 20))[: 1 if operation.startswith("align") else 2] + for extra in scene.selectedItems(): + if extra not in nodes: + extra.setSelected(False) + before = _positions(nodes) + + getattr(scene, operation)() + + assert _positions(nodes) == before + assert not scene.undo_stack.canUndo() From 8087db7ca01bef546cc178fc5b212f44dbacf3b9 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 17:10:28 +0800 Subject: [PATCH 008/166] Test the property panel on a connection and an image --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 8 ++ docs/updates/README.md | 3 +- .../test_utils/test_diagram_property_panel.py | 107 ++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_diagram_property_panel.py diff --git a/architecture_explore.md b/architecture_explore.md index a70822d0..a8600373 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 126 個 `test_*.py`、2278 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 127 個 `test_*.py`、2283 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 763fa3d7..d40b49e5 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -701,3 +701,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2278 tests in 126 files. - **Files**: `test/test_utils/test_diagram_align.py` (new), `architecture_explore.md` §18 - **Open items**: none. + +## U-20260924-278 · 2026-09-24 · Tests for the property panel on a connection and an image · #test #diagram + +- **What**: the same coverage run showed the diagram property panel's connection and image handlers (label, style, colour, width, caption, size) run by no test; only the node side had tests. +- **Change**: `test_diagram_property_panel.py` (5): selecting a connection or an image fills the panel and records no undo step; a connection's style, colour and label follow the panel, one undo step each; steps on its width are one undo step, and undoing it restores the width; an image's width edit keeps its height, and its caption follows. They pass on the code as it is. +- **Result / numbers**: 2283 tests in 127 files. +- **Files**: `test/test_utils/test_diagram_property_panel.py` (new), `architecture_explore.md` §18 +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 229bbdb3..9e9bcf92 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-278 | 2026-09-24 | Tests for the property panel on a connection and an image | #test #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-277 | 2026-09-24 | Tests for the diagram editor's align and distribute | #test #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-276 | 2026-09-24 | Check the trimmed diff against the plain one only on small texts | #fix #diff #perf | [2026-09-b](2026-09-b.md) | | U-20260924-275 | 2026-09-24 | The Traditional and Simplified Chinese READMEs follow README.md again | #docs #readme | [2026-09-b](2026-09-b.md) | @@ -359,4 +360,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 75 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 76 | diff --git a/test/test_utils/test_diagram_property_panel.py b/test/test_utils/test_diagram_property_panel.py new file mode 100644 index 00000000..8cfe00cd --- /dev/null +++ b/test/test_utils/test_diagram_property_panel.py @@ -0,0 +1,107 @@ +"""The property panel for a connection and an image: what it shows, and what an edit there does.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtGui import QColor +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.diagram_editor.diagram_items import ( + ConnectionStyle, DiagramConnection, DiagramImage, DiagramNode, +) +from pybreeze.pybreeze_ui.diagram_editor.diagram_property_panel import DiagramPropertyPanel +from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +@pytest.fixture() +def connection(app): + scene = DiagramScene() + source, target = DiagramNode(x=0, y=0, text="A"), DiagramNode(x=300, y=0, text="B") + scene.addItem(source) + scene.addItem(target) + edge = DiagramConnection(source, target, label="calls", line_width=2) + scene.addItem(edge) + panel = DiagramPropertyPanel(scene) + edge.setSelected(True) + yield scene, edge, panel + panel.deleteLater() + scene.deleteLater() + + +@pytest.fixture() +def image(app): + scene = DiagramScene() + picture = DiagramImage(x=0, y=0, w=120, h=80) + scene.addItem(picture) + panel = DiagramPropertyPanel(scene) + picture.setSelected(True) + yield scene, picture, panel + panel.deleteLater() + scene.deleteLater() + + +def _edges(scene: DiagramScene) -> list[DiagramConnection]: + return [item for item in scene.items() if isinstance(item, DiagramConnection)] + + +class TestAConnection: + def test_selecting_it_shows_it_and_records_nothing(self, connection): + scene, _edge, panel = connection + + assert panel._conn_label.text() == "calls" + assert panel._conn_width.value() == 2 + assert not scene.undo_stack.canUndo() + + def test_its_style_colour_and_label_follow_the_panel(self, connection): + scene, edge, panel = connection + + panel._conn_style.setCurrentIndex(1) + panel._conn_color.set_color(QColor("#ff0000")) + panel._conn_color.color_changed.emit(QColor("#ff0000")) + panel._conn_label.setText("returns") + panel._conn_label.editingFinished.emit() + + assert edge._style == ConnectionStyle.DASHED + assert edge._line_color == QColor("#ff0000") + assert edge.edge_label() == "returns" + assert scene.undo_stack.count() == 3 + + def test_width_steps_are_one_undo_step(self, connection): + scene, _edge, panel = connection + + for width in (3, 4, 5): + panel._conn_width.setValue(width) + scene.undo_stack.undo() + + (restored,) = _edges(scene) + assert restored._line_width == 2 + assert not scene.undo_stack.canUndo() + + +class TestAnImage: + def test_selecting_it_shows_its_size(self, image): + scene, _picture, panel = image + + assert (panel._img_w.value(), panel._img_h.value()) == (120, 80) + assert not scene.undo_stack.canUndo() + + def test_a_width_edit_keeps_its_height_and_the_caption_follows(self, image): + _scene, picture, panel = image + + panel._img_w.setValue(200) + panel._img_caption.setText("logo") + panel._img_caption.editingFinished.emit() + + assert (picture.img_w, picture.img_h) == (200, 80) + assert picture.text() == "logo" From 35aaecdf002dedbfe74d3d32926a27aebd657249 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 17:13:22 +0800 Subject: [PATCH 009/166] Give a new diagram node its text in the IDE language --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 8 +++---- docs/updates/2026-09-b.md | 9 ++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_english.py | 2 ++ .../extend_traditional_chinese.py | 2 ++ .../diagram_editor/diagram_scene.py | 6 ++++-- test/test_utils/test_diagram_editing.py | 21 +++++++++++++++++++ 10 files changed, 47 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c0d37ffd..df49567a 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 731 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 733 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 7e02f3cd..53d89de4 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -211,7 +211,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 731 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 733 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 6eeef2f1..64e9c0b8 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -211,7 +211,7 @@ PyBreeze 沿用 JEditor 的插件架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 731 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯插件加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 733 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯插件加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index a8600373..88963928 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -50,7 +50,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ┌─────────────────────────────────────────────────────────────────────────┐ │ 基礎層 Foundation │ │ pybreeze/utils/ 18 個工具子套件(純邏輯,可單測) │ - │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 731 鍵) │ + │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 733 鍵) │ └─────────────────────────────────────────────────────────────────────────┘ ▼ 外部子行程:python -m je_api_testka / je_auto_control / je_web_runner / @@ -66,7 +66,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 731 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 733 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示 @@ -363,7 +363,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 731 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 733 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 127 個 `test_*.py`、2283 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 127 個 `test_*.py`、2285 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index d40b49e5..c3d85e6b 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -709,3 +709,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2283 tests in 127 files. - **Files**: `test/test_utils/test_diagram_property_panel.py` (new), `architecture_explore.md` §18 - **Open items**: none. + +## U-20260924-279 · 2026-09-24 · A new diagram node's text in the IDE language · #fix #i18n #diagram + +- **What**: a node added with a shape tool said `Node`, and one added with the Text tool `Text`, whatever the IDE spoke. Found reading the diagram scene's untested lines from the coverage run. +- **Fix**: `_add_shape_node` and `_add_text_node` take the text from `diagram_editor_new_node_text` (`Node` / `節點`) and `diagram_editor_new_text_text` (`Text` / `文字`). A saved diagram's text is unchanged. +- **Tests**: `test_diagram_editing.py` +2 (a shape node and a text node, in Traditional Chinese). +- **Result / numbers**: 733 keys in each language. 2285 tests in 127 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_diagram_editing.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 9e9bcf92..005a29ab 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-279 | 2026-09-24 | A new diagram node's text in the IDE language | #fix #i18n #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-278 | 2026-09-24 | Tests for the property panel on a connection and an image | #test #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-277 | 2026-09-24 | Tests for the diagram editor's align and distribute | #test #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-276 | 2026-09-24 | Check the trimmed diff against the plain one only on small texts | #fix #diff #perf | [2026-09-b](2026-09-b.md) | @@ -360,4 +361,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 76 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 77 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 570f4cef..f261e5e6 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -667,6 +667,8 @@ "diagram_editor_tool_diamond": "Diamond", "diagram_editor_tool_connection": "Connect", "diagram_editor_tool_text": "Text", + "diagram_editor_new_node_text": "Node", + "diagram_editor_new_text_text": "Text", # Diagram Editor — Actions "diagram_editor_action_new": "New", "diagram_editor_action_open": "Open", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 1530c0a1..af91c785 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -633,6 +633,8 @@ "diagram_editor_tool_diamond": "菱形", "diagram_editor_tool_connection": "連線", "diagram_editor_tool_text": "文字", + "diagram_editor_new_node_text": "節點", + "diagram_editor_new_text_text": "文字", # Diagram Editor — 動作 "diagram_editor_action_new": "新增", "diagram_editor_action_open": "開啟", diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py index cc85caab..63cb67b9 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py @@ -281,7 +281,9 @@ def mousePressEvent(self, event) -> None: def _add_shape_node(self, pos: QPointF, shape: NodeShape) -> None: with self.undo_scope("Add Node"): - self.addItem(DiagramNode(x=pos.x() - 70, y=pos.y() - 30, shape=shape)) + self.addItem(DiagramNode( + x=pos.x() - 70, y=pos.y() - 30, shape=shape, + text=language_wrapper.language_word_dict.get("diagram_editor_new_node_text", "Node"))) self.item_count_changed.emit() self.mode = ToolMode.SELECT @@ -289,7 +291,7 @@ def _add_text_node(self, pos: QPointF) -> None: with self.undo_scope("Add Text"): self.addItem(DiagramNode( x=pos.x() - 70, y=pos.y() - 20, - w=140, h=40, text="Text", + w=140, h=40, text=language_wrapper.language_word_dict.get("diagram_editor_new_text_text", "Text"), shape=NodeShape.RECTANGLE, )) self.item_count_changed.emit() diff --git a/test/test_utils/test_diagram_editing.py b/test/test_utils/test_diagram_editing.py index 595e071e..43ea2bff 100644 --- a/test/test_utils/test_diagram_editing.py +++ b/test/test_utils/test_diagram_editing.py @@ -295,3 +295,24 @@ def test_a_scale_outside_the_range_can_step_back_toward_it(self, app): assert view.transform().m11() < 8 assert diagram_view._MAX_SCALE < 8 + + +class TestANewNodesText: + """A node the Rectangle or Text tool adds said "Node" or "Text" whatever the IDE spoke.""" + + @pytest.mark.parametrize(("add", "expected"), [("shape", "節點"), ("text", "文字")]) + def test_is_in_the_ide_language(self, app, monkeypatch, add, expected): + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + from pybreeze.pybreeze_ui.diagram_editor.diagram_items import NodeShape + + monkeypatch.setattr(scene_module.language_wrapper, "language_word_dict", pybreeze_traditional_chinese_word_dict) + scene = DiagramScene() + if add == "shape": + scene._add_shape_node(QPointF(0, 0), NodeShape.RECTANGLE) + else: + scene._add_text_node(QPointF(0, 0)) + + (node,) = [item for item in scene.items() if isinstance(item, DiagramNode)] + assert node.text() == expected From dc0c1bc500272df393ec47721267e7116c07c1ac Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 17:29:04 +0800 Subject: [PATCH 010/166] Read no cookie file for curl -b '' --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- pybreeze/utils/curl_import/curl_parser.py | 6 ++++-- test/test_utils/test_curl_import.py | 9 +++++++++ test/test_utils/test_curl_request_fidelity.py | 6 ++++++ 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 88963928..78cf54b4 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 127 個 `test_*.py`、2285 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 127 個 `test_*.py`、2287 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index c3d85e6b..2ac702a7 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -718,3 +718,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 733 keys in each language. 2285 tests in 127 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_diagram_editing.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-280 · 2026-09-24 · curl -b '' reads no cookie file · #fix #curl + +- **What**: `curl -b ''` only switches curl's cookie engine on and reads no file (curl's manual: an empty file name as the only cookie input starts the engine with no cookies). The cURL import took it as a cookie file named `""`: the generated Python scripts said `# curl read cookies from "" (-b)`, and the APITestka JSON action refused the command ("cannot ... read ... cookies from" a file). Found by a Hypothesis run over generated curl commands and every template target. +- **Fix**: `_apply_cookie` records a `-b` value with no `=` as a cookie file only when it is not empty. +- **Tests**: `test_curl_import.py` +1 (no file, no cookie, no note), `test_curl_request_fidelity.py` +1 (the JSON action is made). The first failed on the old code. The ad-hoc fuzz (20000 commands, 21 flags, all 5 targets parse or compile, only the JSON action may refuse) passes. +- **Result / numbers**: 2287 tests in 127 files. Full suite at 35aaecd: 2270 passed, 15 skipped. `ruff check` clean. +- **Files**: `pybreeze/utils/curl_import/curl_parser.py`, `test/test_utils/test_curl_import.py`, `test/test_utils/test_curl_request_fidelity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 005a29ab..0202b40c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-280 | 2026-09-24 | curl -b '' reads no cookie file | #fix #curl | [2026-09-b](2026-09-b.md) | | U-20260924-279 | 2026-09-24 | A new diagram node's text in the IDE language | #fix #i18n #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-278 | 2026-09-24 | Tests for the property panel on a connection and an image | #test #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-277 | 2026-09-24 | Tests for the diagram editor's align and distribute | #test #diagram | [2026-09-b](2026-09-b.md) | @@ -361,4 +362,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 77 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 78 | diff --git a/pybreeze/utils/curl_import/curl_parser.py b/pybreeze/utils/curl_import/curl_parser.py index 2314d302..c7fb0c65 100644 --- a/pybreeze/utils/curl_import/curl_parser.py +++ b/pybreeze/utils/curl_import/curl_parser.py @@ -401,10 +401,12 @@ def _apply_cookie(request: CurlRequest, value: str) -> None: ``curl -b 'a=1; b=2'`` yields inline cookies; a value with no ``=`` is a cookie *file* curl reads. It was sent as the header ``Cookie: cookies.txt``; - it is kept in ``cookie_files`` for the generators to say so. + it is kept in ``cookie_files`` for the generators to say so. An empty one + (``-b ''``) only switches curl's cookie engine on and reads no file. """ if "=" not in value: - request.cookie_files.append(value) + if value: + request.cookie_files.append(value) return for segment in value.split(";"): name, separator, cookie_value = segment.strip().partition("=") diff --git a/test/test_utils/test_curl_import.py b/test/test_utils/test_curl_import.py index 681c61e6..9c272580 100644 --- a/test/test_utils/test_curl_import.py +++ b/test/test_utils/test_curl_import.py @@ -402,6 +402,15 @@ def test_a_cookie_file_is_kept_as_a_file_not_sent_as_a_cookie(self): assert "Cookie" not in request.headers assert request.cookie_files == ["cookies.txt"] + def test_an_empty_cookie_file_reads_nothing(self): + # curl -b '' only switches on the cookie engine; it reads no file. It + # was a file named "", which the scripts noted and the APITestka + # action refused to generate. + request = parse_curl("curl -b '' https://x") + assert request.cookie_files == [] + assert request.cookies == {} + assert "curl read cookies" not in to_requests_code(request) + def test_no_cookies_by_default(self): assert parse_curl("curl https://x").cookies == {} diff --git a/test/test_utils/test_curl_request_fidelity.py b/test/test_utils/test_curl_request_fidelity.py index f4f94ed1..b94c0f2e 100644 --- a/test/test_utils/test_curl_request_fidelity.py +++ b/test/test_utils/test_curl_request_fidelity.py @@ -183,6 +183,12 @@ def test_the_json_action_refuses_it(self): with pytest.raises(CurlParseException): to_apitestka_action_json(parse_curl("curl -b cookies.txt https://h/")) + def test_an_empty_name_is_no_file_so_the_json_action_is_made(self): + # curl -b '' reads no file: it only switches the cookie engine on + from pybreeze.utils.curl_import.script_templates import to_apitestka_action_json + + assert '"https://h/"' in to_apitestka_action_json(parse_curl("curl -b '' https://h/")) + @pytest.mark.parametrize("url", [ "https://h/a?flag&q=%B0&r=/x&s=a,b", From 96f287e0ae4e1be292b6839426ba6d2442f372df Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 17:38:34 +0800 Subject: [PATCH 011/166] Re-measure the line counts in architecture_explore.md --- architecture_explore.md | 6 +++--- docs/updates/2026-09-b.md | 7 +++++++ docs/updates/README.md | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 78cf54b4..398e4e84 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -239,7 +239,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(3,963 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(3,985 行,最大子系統) | 檔案 | 職責 | |---|---| @@ -294,7 +294,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 9. `pybreeze_ui/connect_gui/` -### `ssh/`(2,035 行) +### `ssh/`(2,230 行) | 檔案 | 職責 | |---|---| @@ -328,7 +328,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 11. `pybreeze_ui/syntax/` -- `syntax_keyword.py`(625 行)— 七份關鍵字清單,彙整成 `package_keyword_list`: +- `syntax_keyword.py`(629 行)— 七份關鍵字清單,彙整成 `package_keyword_list`: `je_auto_control` / `je_load_density` / `je_api_testka` / `je_web_runner` / `automation_file` / `mail_thunder` / `test_pioneer` - `syntax_extend.py` — 把前六個註冊到 `.json`(黃色 `#FFFF00`),`test_pioneer` 註冊到 `TEST_PIONEER_SUFFIXES` 的每個副檔名(`.yml`、`.yaml`,橘色 `#FF9900`),然後重置當前編輯器的 highlighter diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 2ac702a7..bbace6f6 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -727,3 +727,10 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2287 tests in 127 files. Full suite at 35aaecd: 2270 passed, 15 skipped. `ruff check` clean. - **Files**: `pybreeze/utils/curl_import/curl_parser.py`, `test/test_utils/test_curl_import.py`, `test/test_utils/test_curl_request_fidelity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-281 · 2026-09-24 · architecture_explore.md line counts re-measured · #docs + +- **What**: three line counts `architecture_explore.md` quotes had fallen behind the code: `diagram_editor/` 3,963 (now 3,985), `connect_gui/ssh/` 2,035 (now 2,230, after the SFTP work) and `syntax/syntax_keyword.py` 625 (now 629, after `.yaml` highlighting). +- **Fix**: re-measured with `wc -l` and corrected. No other count in the file quotes a line total. +- **Files**: `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0202b40c..8598afb5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-281 | 2026-09-24 | architecture_explore.md line counts re-measured | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-280 | 2026-09-24 | curl -b '' reads no cookie file | #fix #curl | [2026-09-b](2026-09-b.md) | | U-20260924-279 | 2026-09-24 | A new diagram node's text in the IDE language | #fix #i18n #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-278 | 2026-09-24 | Tests for the property panel on a connection and an image | #test #diagram | [2026-09-b](2026-09-b.md) | @@ -362,4 +363,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 78 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 79 | From ca3f81e77241a3a38d6b1e545cb0e809537d7698 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 18:31:56 +0800 Subject: [PATCH 012/166] Open no canvas menu at the end of a right-drag pan --- architecture_explore.md | 24 +-- docs/updates/2026-09-b.md | 10 ++ docs/updates/README.md | 3 +- .../diagram_editor/diagram_view.py | 49 +++++- test/test_utils/test_diagram_view_pan.py | 159 ++++++++++++++++++ 5 files changed, 224 insertions(+), 21 deletions(-) create mode 100644 test/test_utils/test_diagram_view_pan.py diff --git a/architecture_explore.md b/architecture_explore.md index 398e4e84..6360efd6 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -239,18 +239,18 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(3,985 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,018 行,最大子系統) | 檔案 | 職責 | |---|---| -| `diagram_editor_widget.py` (678) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | -| `diagram_scene.py` (888) | `DiagramScene(QGraphicsScene)`:**State pattern** 的 `ToolMode` 決定滑鼠行為;undo/redo、複製貼上(節點、連線與圖片)、多選對齊與分佈、z-order、序列化 `to_dict()` / `load_from_dict()`。`get_all_nodes/connections/images()` 由下往上列出(`_bottom_first()`),存檔與 undo 還原時同 z 值的重疊項目維持原本的上下;右鍵選單先選取點到的項目;置頂/置底放到所有其他節點與圖片之上/之下;`to_dict()` 給每個節點與圖片記下它在兩者之間由下往上的位置(`stack`),載入後 `_restore_stacking()` 照這個順序重新加回場景(同 z 值時後加的在上面),圖片也存 `z`。`load_from_dict()` 先用 `_check_is_a_diagram()` 確認資料形狀才清空畫布(不合就丟 `ValueError`,畫布原封不動),每一筆節點/連線/圖片再各自容錯;清空前先 `to_dict()` 留一份,載入途中還是出錯就放回原樣再往上丟——載入要嘛成功、要嘛什麼都沒變(編輯器存檔寫回上次開的檔案,半途清空的畫布會蓋掉使用者的檔)。圖片的 `source` 不是字串就丟掉。`undo_scope` 用 `try/finally`,本體丟例外也一定收掉快照;圖片來源先看副檔名、拒絕 UNC(`_is_on_this_machine()`)才碰檔案系統;URL 圖片交給 `ImageDownloadThread(QThread)` 下載並快取在 `_pixmap_cache`,undo/redo 重建項目時直接用快取,不會再連一次網路;編輯器關閉時 `let_image_downloads_run_out()` 把還在跑的下載交給 `let_run_out()`,不在 UI 執行緒等 | -| `diagram_items.py` (960) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | -| `diagram_mermaid_parser.py` (603) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | +| `diagram_editor_widget.py` (679) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | +| `diagram_scene.py` (915) | `DiagramScene(QGraphicsScene)`:**State pattern** 的 `ToolMode` 決定滑鼠行為;undo/redo、複製貼上(節點、連線與圖片)、多選對齊與分佈、z-order、序列化 `to_dict()` / `load_from_dict()`。`get_all_nodes/connections/images()` 由下往上列出(`_bottom_first()`),存檔與 undo 還原時同 z 值的重疊項目維持原本的上下;右鍵選單先選取點到的項目;置頂/置底放到所有其他節點與圖片之上/之下;`to_dict()` 給每個節點與圖片記下它在兩者之間由下往上的位置(`stack`),載入後 `_restore_stacking()` 照這個順序重新加回場景(同 z 值時後加的在上面),圖片也存 `z`。`load_from_dict()` 先用 `_check_is_a_diagram()` 確認資料形狀才清空畫布(不合就丟 `ValueError`,畫布原封不動),每一筆節點/連線/圖片再各自容錯;清空前先 `to_dict()` 留一份,載入途中還是出錯就放回原樣再往上丟——載入要嘛成功、要嘛什麼都沒變(編輯器存檔寫回上次開的檔案,半途清空的畫布會蓋掉使用者的檔)。圖片的 `source` 不是字串就丟掉。`undo_scope` 用 `try/finally`,本體丟例外也一定收掉快照;圖片來源先看副檔名、拒絕 UNC(`_is_on_this_machine()`)才碰檔案系統;URL 圖片交給 `ImageDownloadThread(QThread)` 下載並快取在 `_pixmap_cache`,undo/redo 重建項目時直接用快取,不會再連一次網路;編輯器關閉時 `let_image_downloads_run_out()` 把還在跑的下載交給 `let_run_out()`,不在 UI 執行緒等 | +| `diagram_items.py` (965) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | +| `diagram_mermaid_parser.py` (613) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | | `diagram_property_panel.py` (453) | 右側屬性側欄,依選取型別切換 node / connection / image 三組表單;每記一步 undo(場景的 `recorded`)就重新整理(在畫布上拖把手改大小時選取沒變);寬、高各自只改自己那一邊;數字欄位不追鍵盤、輸入完才套用 | -| `diagram_view.py` (195) | `QGraphicsView`:滾輪與按鈕縮放都經 `_step_zoom()`(有上下界,界外時仍可往界內走;橫向滾輪不縮放),`fit()` 把「符合視窗」夾在上下界內、中鍵平移、`drawBackground` 畫格線 | +| `diagram_view.py` (228) | `QGraphicsView`:滾輪與按鈕縮放都經 `_step_zoom()`(有上下界,界外時仍可往界內走;橫向滾輪不縮放),`fit()` 把「符合視窗」夾在上下界內、中鍵或右鍵拖曳平移(右鍵拖過畫布就不開右鍵選單,`contextMenuEvent`;放開沒送到它時下一次移動就停止平移)、`drawBackground` 畫格線 | | `diagram_commands.py` (48) | `DiagramSnapshotCommand(QUndoCommand)` — 快照式 undo,存變更前後完整場景狀態;有 `merge_key` 的連續步驟(同一個項目的同一個屬性:大小、字級、線寬)合併成一步(`id()` / `mergeWith`) | -| `diagram_net_utils.py` (112) | **SSRF 防護參考實作**:scheme 白名單、DNS 解析後比對私有/迴環/link-local/reserved 網段、`_ValidatingRedirectHandler` 對每一跳重驗(驗過就關掉轉址回應,urllib 不會把轉址的內容整個讀完)、`_OPENER` 用 `PublicHTTPHandler` / `PublicHTTPSHandler`(連線當下再檢查一次並只連到那個位址)、20 MB 大小上限、每次等資料 15 秒 timeout,整個下載另有 `overall_deadline(DOWNLOAD_DEADLINE_SECONDS)` 120 秒上限(慢慢送位元組的伺服器原本可以一直卡住下載執行緒) | +| `diagram_net_utils.py` (117) | **SSRF 防護參考實作**:scheme 白名單、DNS 解析後比對私有/迴環/link-local/reserved 網段、`_ValidatingRedirectHandler` 對每一跳重驗(驗過就關掉轉址回應,urllib 不會把轉址的內容整個讀完)、`_OPENER` 用 `PublicHTTPHandler` / `PublicHTTPSHandler`(連線當下再檢查一次並只連到那個位址)、20 MB 大小上限、每次等資料 15 秒 timeout,整個下載另有 `overall_deadline(DOWNLOAD_DEADLINE_SECONDS)` 120 秒上限(慢慢送位元組的伺服器原本可以一直卡住下載執行緒) | `diagram_net_utils` 是 CLAUDE.md 指定的網路安全參考實作,其他 HTTP 呼叫端則統一走 `utils/network/url_validation.py` 驗證、經 `utils/network/public_http.py` 的 `public_session()` 送出。 @@ -301,7 +301,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | | `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | -| `ssh_file_viewer_widget.py` (722) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | +| `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | | `ssh_key_loader.py` | 依序嘗試各種私鑰型別,回傳第一個能解析的;都不行時 `unloadable_key_reason()` 分辨是密語沒給/給錯(檔案有加密,而且給的密語解不開它;用 `cryptography` 試解)還是不支援的私鑰(例如加密的 DSA) | @@ -348,9 +348,9 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 | `network/url_validation.py` | `validate_url()`:先拒絕 `urlparse` 與 `urllib3` 讀出不同主機的 URL(反斜線、空白、控制字元,或兩者主機不同;`_check_one_reading`),再做 scheme 白名單、私有/迴環/link-local/reserved 阻擋、額外處理 CGNAT 與 NAT64 網段、IPv6 內嵌 IPv4 的偵測 | | `network/public_http.py` | 只連到剛檢查過的位址(防 DNS rebinding):`public_session()`(`_NoRedirectSession`:不跟也不準備轉址,3xx 原封不讀地回來;`PublicAddressAdapter`,連線開 socket 時把 urllib3 的 `_dns_host` 依序暫換成 `public_addresses()` 回傳的每個位址,連得上就用,全部失敗才丟最後一個錯誤)、`PublicHTTPHandler` / `PublicHTTPSHandler`(`http.client` 的 `_create_connection`)。主機名仍是連線的 host,所以 SNI、憑證檢查與 `Host` 標頭照舊;經 proxy 的連線不釘住。`overall_deadline(seconds)`:這個執行緒在區塊內的請求總共最多這麼久,釘住的連線等回應時把 socket 登記上去,時間到就 shutdown,丟 `ReadTimeout`(讀取逾時每來一個位元組就重算,慢慢送標頭的伺服器原本可以一直拖);AI 審查、Skill、CoT 每一步都包在 `overall_deadline(DEFAULT_MAX_READ_SECONDS)` 裡。`test_http_goes_through_public_connections.py` 擋下直接呼叫 `requests.*` / `urlopen` | | `network/http_client.py` | `read_capped_text()`(串流讀取有上限,超出丟 `ResponseTooLargeError`;照 `Content-Type` 明寫的 charset 解碼,沒寫就 UTF-8,不用 requests 給 `text/*` 的 ISO-8859-1,`named_charset()`;整個回應最多讀 `DEFAULT_MAX_READ_SECONDS`(300 秒),時間到由 `_Watchdog` 關掉連線(urllib3 的 `HTTPResponse.shutdown()` 能中斷別的執行緒上正在等的讀取),丟 `ReadTimeout`:讀取逾時只管每一塊之間,一次送一個位元組的伺服器可以一直拖下去;狀態列與標頭由呼叫端的 `public_http.overall_deadline()` 涵蓋)、`describe_request_error()`(給使用者看的失敗原因:逾時、連不上、URL 不合法等,不含 URL;requests 的錯誤訊息會引用含 token 的完整 URL)、`succeeded()`(只有 2xx 算回答;`response.ok` 連 3xx 都算,這些請求又不跟隨轉址)、`truncate_for_display()`、`CONNECT_TIMEOUT` | -| `curl_import/` | `curl_parser.py`(600) 完整 curl 解析(`-I` 是 HEAD、`--oauth2-bearer` 變成 `Authorization`(`-H` 給的優先)、URL 的 `#fragment` 丟掉、URL 拆不開(沒關的 `[`、不是數字的 port)就是 `CurlParseException`,`url_is_well_formed()` 也給 HAR 用:這種 entry 跳過;同名的 `-F` 全留(`request_body.py` 的 `form_parts()` 回傳 list,產生的程式寫成 `files=[(…), …]`);表單一律以 multipart 送出:文字欄位也放進 `files=`,寫成 `(None, 文字)`(`form_entries()`),複製來的 `Content-Type: multipart/...` 不寫進 headers(`sent_headers()`,requests 要自己帶 boundary);APITestka JSON action 遇到上傳檔案、`@file` body 或 `-b` cookie 檔就丟 `CurlParseException`;`@file` body 一律以位元組讀(`--data-binary` 原樣、`-d` 去掉 CR/LF,和 curl 一樣),和其他 `-d` 片段照命令列的順序接起來(`data_file_positions`);`-b <檔案>` 存進 `cookie_files`,產生的程式以註解說明沒有讀它;`-G` 搭 `@file` 拒絕;bash 的 `$'...'` 先展開成一般引號字串再交給 `shlex`、短旗標叢集展開、`--data-urlencode`、`-F`、`--form-string`、`-b`、`-G`);`-F` 的值照 curl 語法(`@` 開頭是上傳檔案),`--form-string` 與 HAR 的文字欄位進 `form_strings`、照字面;URL 的 query 只有「解碼再編碼會一模一樣」時才拆進 `params`(`query_tools.query_round_trips()`,URL Builder 也用它決定 query 顯示成 dict 還是原字串;否則照原樣留在 URL,`requests` 原封送出,簽章 URL 才不會壞),query 參數 `params` 同一個 key 出現多次時存成值的清單(`add_repeated_value()`),URL 的在前、`-G` 的在後,和 curl 實際送出的一樣;`http_method()` 只收 RFC 9110 的 token(HAR 也用它),方法會寫進產生的程式碼,不是 token 就拒絕;`request_body.py` 判斷 body 型別(`body_kind()`:JSON 物件要能原樣送回才走 `json=`,重複的 key、float 裝不下的數字、`NaN`、巢狀超過 100 層都照原字串送);`request_codegen.py` 產 requests 程式(字串一律經 `python_string()`:`json.dumps` 預設把 BMP 以外的字元寫成兩個 surrogate,Python 讀成兩個字;JSON body 經 `python_literal()` 寫成 Python,`true`/`null` 在 Python 裡是未定義的名稱);`script_templates.py`(293) 產 APITestka/LoadDensity/pytest 模板 | -| `har_import/` | `har_parser.py`(354) HAR → `CurlRequest`(重用 curl 那套 codegen;建不出請求的 entry 跳過,含半個字元(JSON 的 `\ud800`)的也跳過,同名 cookie 改走 `Cookie` header);`is_api_like()` 濾掉靜態資源;`har_codegen.py` 批次產生單一腳本、函式名去重,每段開頭註解裡的控制字元寫成 `\xNN`(URL 裡的換行不會結束註解) | -| `header_tools/` | `header_analyzer.py`(318) 安全稽核;`header_merge.py` 依 HTTP 規則合併重複 header(Cookie 用 `; ` 其餘用 `, `) | +| `curl_import/` | `curl_parser.py`(612) 完整 curl 解析(`-I` 是 HEAD、`--oauth2-bearer` 變成 `Authorization`(`-H` 給的優先)、URL 的 `#fragment` 丟掉、URL 拆不開(沒關的 `[`、不是數字的 port)就是 `CurlParseException`,`url_is_well_formed()` 也給 HAR 用:這種 entry 跳過;同名的 `-F` 全留(`request_body.py` 的 `form_parts()` 回傳 list,產生的程式寫成 `files=[(…), …]`);表單一律以 multipart 送出:文字欄位也放進 `files=`,寫成 `(None, 文字)`(`form_entries()`),複製來的 `Content-Type: multipart/...` 不寫進 headers(`sent_headers()`,requests 要自己帶 boundary);APITestka JSON action 遇到上傳檔案、`@file` body 或 `-b` cookie 檔就丟 `CurlParseException`;`@file` body 一律以位元組讀(`--data-binary` 原樣、`-d` 去掉 CR/LF,和 curl 一樣),和其他 `-d` 片段照命令列的順序接起來(`data_file_positions`);`-b <檔案>` 存進 `cookie_files`,產生的程式以註解說明沒有讀它;`-G` 搭 `@file` 拒絕;bash 的 `$'...'` 先展開成一般引號字串再交給 `shlex`、短旗標叢集展開、`--data-urlencode`、`-F`、`--form-string`、`-b`、`-G`);`-F` 的值照 curl 語法(`@` 開頭是上傳檔案),`--form-string` 與 HAR 的文字欄位進 `form_strings`、照字面;URL 的 query 只有「解碼再編碼會一模一樣」時才拆進 `params`(`query_tools.query_round_trips()`,URL Builder 也用它決定 query 顯示成 dict 還是原字串;否則照原樣留在 URL,`requests` 原封送出,簽章 URL 才不會壞),query 參數 `params` 同一個 key 出現多次時存成值的清單(`add_repeated_value()`),URL 的在前、`-G` 的在後,和 curl 實際送出的一樣;`http_method()` 只收 RFC 9110 的 token(HAR 也用它),方法會寫進產生的程式碼,不是 token 就拒絕;`request_body.py` 判斷 body 型別(`body_kind()`:JSON 物件要能原樣送回才走 `json=`,重複的 key、float 裝不下的數字、`NaN`、巢狀超過 100 層都照原字串送);`request_codegen.py` 產 requests 程式(字串一律經 `python_string()`:`json.dumps` 預設把 BMP 以外的字元寫成兩個 surrogate,Python 讀成兩個字;JSON body 經 `python_literal()` 寫成 Python,`true`/`null` 在 Python 裡是未定義的名稱);`script_templates.py`(293) 產 APITestka/LoadDensity/pytest 模板 | +| `har_import/` | `har_parser.py`(368) HAR → `CurlRequest`(重用 curl 那套 codegen;建不出請求的 entry 跳過,含半個字元(JSON 的 `\ud800`)的也跳過,同名 cookie 改走 `Cookie` header);`is_api_like()` 濾掉靜態資源;`har_codegen.py` 批次產生單一腳本、函式名去重,每段開頭註解裡的控制字元寫成 `\xNN`(URL 裡的換行不會結束註解) | +| `header_tools/` | `header_analyzer.py`(335) 安全稽核;`header_merge.py` 依 HTTP 規則合併重複 header(Cookie 用 `; ` 其餘用 `, `) | | `jwt_tools/`、`hash_tools/`、`timestamp_tools/`、`regex_tools/`、`query_tools/`、`url_tools/`、`diff_tools/`、`http_reference/`、`json_format/`、`response_inspector/` | 對應 §6 工具分頁的純邏輯。`json_format` 的 Format / Minify 不改內容:數字保留原文(先換成帶隨機標記的佔位字串、輸出後一次換回)、非 ASCII 原樣輸出、同一物件重複的 key 與 `NaN`/`Infinity` 報錯;`pretty_json_or_none()` 是同一套解析、不記 log 的版本,Response Inspector 的 body 與 JWT 各段(`jwt_decoder.shown_json()`)用它排版。`json_format/view_safe.py` 的 `dumps_for_view()` / `escape_for_view()`:工具顯示的 JSON 把文字框還不回原樣的字元(U+2029、U+FDD0、U+FDD1 會變換行,落單的 surrogate 會消失;U+2028 經 `toPlainText()`、U+0085 經 `splitlines()` 也會斷行)寫成 `\uXXXX`,JSON Format、Query/URL 轉 JSON、JWT、Response Inspector、cURL/HAR 產生的 JSON 與 Python 字串都經過它。`query_tools` 與 `url_tools` 讀 JSON 也保留數字原文、拒收 `NaN` 與同一物件重複的 key(`load_json_verbatim()`,用 `json_process.unique_pairs`),Query 轉 JSON 遇到不是 UTF-8 的 percent-escape 報錯而不換成 U+FFFD,`urlencode` 經 `encode_pairs()`:寫不進 URL 的字元(落單的 surrogate)報自己的錯,不從分頁的 slot 漏出去 | | `file_process/get_dir_file_list.py` | 遞迴收集指定副檔名的檔案(大小寫不敏感) | | `file_process/read_capped.py` | `read_text_capped(path, encoding, max_bytes=None)`:先看大小,超過 `MAX_OPEN_BYTES`(100 MB)丟 `FileTooLargeError`(`OSError`,`strerror` 說明大小與上限),HAR 分頁與架構圖編輯器開檔都經它,不在 UI 執行緒讀好幾 GB 的檔案 | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 127 個 `test_*.py`、2287 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2293 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index bbace6f6..3b74b7ad 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -734,3 +734,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Fix**: re-measured with `wc -l` and corrected. No other count in the file quotes a line total. - **Files**: `architecture_explore.md` - **Open items**: none. + +## U-20260924-282 · 2026-09-24 · A right-drag on the diagram canvas pans without opening the menu · #fix #diagram + +- **What**: the diagram canvas pans with the middle or the right button (`DiagramView.mousePressEvent`), and the scene has a right-click menu. Windows opens a context menu as the right button is released, so every right-drag pan ended with the canvas menu open. Where the menu opens on the press (Linux, macOS), the menu took the release, `_panning` stayed on, and the canvas went on scrolling with the mouse and no button held. Found reading the view's untested lines; reproduced headless by sending the mouse through the window, where Qt makes the context-menu event. +- **Fix**: `DiagramView` remembers the pan button. A right-drag that moved at least the system drag distance swallows the one mouse-triggered context menu that follows it (`contextMenuEvent`). A right click that did not move and the keyboard's menu key still open it. A move with the pan button no longer held stops the pan (`_stop_panning`). +- **Tests**: new `test/test_utils/test_diagram_view_pan.py` (6): a right-drag pans and opens no menu; a right click still opens it; a right click after a right-drag opens it; a middle-drag pans; the menu key after a right-drag opens it; a pan whose release went to a menu opened on the press stops on the next move. The trigger is set per test (`QStyleHints.setContextMenuTrigger`). Four failed on the old code. +- **Docs**: `architecture_explore.md`'s `diagram_view.py` row describes the right-drag. The per-file `(N)` line counts in its tables were re-measured too: ten were behind, which U-20260924-281 missed (it checked only the `N 行` form). `diagram_editor/` is now 4,018 lines. +- **Result / numbers**: 2293 tests in 128 files. The diagram tests (148) pass. `ruff check` and C901 clean. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_view.py`, `test/test_utils/test_diagram_view_pan.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8598afb5..6fff300f 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-282 | 2026-09-24 | A right-drag on the diagram canvas pans without opening the menu | #fix #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-281 | 2026-09-24 | architecture_explore.md line counts re-measured | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-280 | 2026-09-24 | curl -b '' reads no cookie file | #fix #curl | [2026-09-b](2026-09-b.md) | | U-20260924-279 | 2026-09-24 | A new diagram node's text in the IDE language | #fix #i18n #diagram | [2026-09-b](2026-09-b.md) | @@ -363,4 +364,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 79 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 80 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_view.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_view.py index c8ff1ef9..69315fd4 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_view.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_view.py @@ -1,8 +1,8 @@ from __future__ import annotations from PySide6.QtCore import QRectF, Qt, Signal -from PySide6.QtGui import QBrush, QColor, QPainter, QPen -from PySide6.QtWidgets import QGraphicsView +from PySide6.QtGui import QBrush, QColor, QContextMenuEvent, QPainter, QPen +from PySide6.QtWidgets import QApplication, QGraphicsView from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene @@ -12,6 +12,7 @@ _GRID_COLOR = QColor("#e0e0e0") _GRID_COLOR_MAJOR = QColor("#bdbdbd") _BG_COLOR = QColor("#ffffff") +_PAN_BUTTONS = (Qt.MouseButton.MiddleButton, Qt.MouseButton.RightButton) class DiagramView(QGraphicsView): @@ -54,6 +55,10 @@ def __init__(self, scene: DiagramScene, parent=None): self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) self._panning = False self._pan_start = None + self._pan_origin = None + self._pan_button = Qt.MouseButton.NoButton + # A right-drag moved the canvas: the menu its release brings is not wanted + self._right_drag_panned = False self._draw_grid = False self._grid_size = 20 @@ -168,28 +173,56 @@ def zoom_out(self) -> None: # --- middle-button / right-button pan --- def mousePressEvent(self, event) -> None: - if event.button() in (Qt.MouseButton.MiddleButton, Qt.MouseButton.RightButton): + if event.button() in _PAN_BUTTONS: self._panning = True + self._pan_button = event.button() self._pan_start = event.position().toPoint() + self._pan_origin = self._pan_start + self._right_drag_panned = False self.setCursor(Qt.CursorShape.ClosedHandCursor) event.accept() return super().mousePressEvent(event) def mouseMoveEvent(self, event) -> None: + if self._panning and not event.buttons() & self._pan_button: + # The release went elsewhere (a context menu opened on the press + # took it): the canvas kept following a mouse with no button held + self._stop_panning() if self._panning and self._pan_start is not None: - delta = event.position().toPoint() - self._pan_start - self._pan_start = event.position().toPoint() + position = event.position().toPoint() + delta = position - self._pan_start + self._pan_start = position self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x()) self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y()) + if (self._pan_button == Qt.MouseButton.RightButton + and (position - self._pan_origin).manhattanLength() >= QApplication.startDragDistance()): + self._right_drag_panned = True event.accept() return super().mouseMoveEvent(event) def mouseReleaseEvent(self, event) -> None: - if event.button() in (Qt.MouseButton.MiddleButton, Qt.MouseButton.RightButton) and self._panning: - self._panning = False - self.setCursor(Qt.CursorShape.ArrowCursor) + if event.button() == self._pan_button and self._panning: + self._stop_panning() event.accept() return super().mouseReleaseEvent(event) + + def _stop_panning(self) -> None: + self._panning = False + self._pan_button = Qt.MouseButton.NoButton + self.setCursor(Qt.CursorShape.ArrowCursor) + + def contextMenuEvent(self, event: QContextMenuEvent) -> None: + """Open the canvas menu, except at the end of a right-drag that panned. + + Windows opens a context menu as the right button is released, so every + pan with the right button ended in a menu. A right click that did not + move, and the keyboard's menu key, still open it. + """ + panned, self._right_drag_panned = self._right_drag_panned, False + if panned and event.reason() == QContextMenuEvent.Reason.Mouse: + event.accept() + return + super().contextMenuEvent(event) diff --git a/test/test_utils/test_diagram_view_pan.py b/test/test_utils/test_diagram_view_pan.py new file mode 100644 index 00000000..b7fe6d68 --- /dev/null +++ b/test/test_utils/test_diagram_view_pan.py @@ -0,0 +1,159 @@ +"""Panning the diagram canvas with the right or middle button, and the menu a right click opens.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtCore import QEvent, QPoint, QPointF, Qt +from PySide6.QtGui import QContextMenuEvent, QGuiApplication, QMouseEvent +from PySide6.QtTest import QTest +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.diagram_editor import diagram_scene as scene_module +from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene +from pybreeze.pybreeze_ui.diagram_editor.diagram_view import DiagramView + +_NO_KEYS = Qt.KeyboardModifier.NoModifier + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +@pytest.fixture +def menus(monkeypatch) -> list: + """Every canvas menu that opens, recorded instead of shown.""" + opened: list = [] + + class Menu: + def __init__(self, *_args) -> None: + self.entries: list = [] + + def addAction(self, *entry) -> None: + self.entries.append(entry) + + def addSeparator(self) -> None: + """Separators are not choices.""" + + def actions(self) -> list: + return self.entries + + def exec(self, *_args) -> None: + opened.append(self) + + monkeypatch.setattr(scene_module, "QMenu", Menu) + return opened + + +@pytest.fixture +def menu_on_release(app): + """Open context menus as Windows does, on the right button's release.""" + hints = QGuiApplication.styleHints() + before = hints.contextMenuTrigger() + hints.setContextMenuTrigger(Qt.ContextMenuTrigger.Release) + yield + hints.setContextMenuTrigger(before) + + +@pytest.fixture +def menu_on_press(app): + """Open context menus as Linux and macOS do, on the right button's press.""" + hints = QGuiApplication.styleHints() + before = hints.contextMenuTrigger() + hints.setContextMenuTrigger(Qt.ContextMenuTrigger.Press) + yield + hints.setContextMenuTrigger(before) + + +@pytest.fixture +def view(app): + scene = DiagramScene() + scene.setSceneRect(0, 0, 3000, 3000) + shown = DiagramView(scene) + shown.resize(400, 300) + shown.show() + assert QTest.qWaitForWindowExposed(shown) + yield shown + shown.close() + shown.deleteLater() + + +def _at(view: DiagramView, x: int, y: int) -> QPoint: + """A point on the canvas, in the window's coordinates.""" + return view.viewport().mapTo(view.window(), QPoint(x, y)) + + +def _drag(view: DiagramView, button: Qt.MouseButton, start: tuple, end: tuple) -> None: + # Through the window, as the platform delivers them: that is where Qt + # turns a right button into a context menu + window = view.windowHandle() + QTest.mousePress(window, button, _NO_KEYS, _at(view, *start)) + QTest.mouseMove(window, _at(view, (start[0] + end[0]) // 2, (start[1] + end[1]) // 2)) + QTest.mouseMove(window, _at(view, *end)) + QTest.mouseRelease(window, button, _NO_KEYS, _at(view, *end)) + QApplication.processEvents() + + +def test_a_right_drag_pans_and_opens_no_menu(view, menus, menu_on_release): + left = view.horizontalScrollBar().value() + + _drag(view, Qt.MouseButton.RightButton, (200, 150), (100, 100)) + + assert view.horizontalScrollBar().value() == left + 100 + # Every right-drag used to end in the canvas menu + assert menus == [] + + +def test_a_right_click_still_opens_the_menu(view, menus, menu_on_release): + _drag(view, Qt.MouseButton.RightButton, (200, 150), (200, 150)) + + assert len(menus) == 1 + + +def test_a_right_click_after_a_right_drag_opens_the_menu(view, menus, menu_on_release): + _drag(view, Qt.MouseButton.RightButton, (200, 150), (100, 100)) + _drag(view, Qt.MouseButton.RightButton, (50, 50), (50, 50)) + + assert len(menus) == 1 + + +def test_a_middle_drag_pans(view, menus, menu_on_release): + top = view.verticalScrollBar().value() + + _drag(view, Qt.MouseButton.MiddleButton, (200, 150), (200, 90)) + + assert view.verticalScrollBar().value() == top + 60 + assert menus == [] + + +def test_the_menu_key_after_a_right_drag_opens_the_menu(view, menus, menu_on_release): + _drag(view, Qt.MouseButton.RightButton, (200, 150), (100, 100)) + + at = QPoint(50, 50) + key = QContextMenuEvent(QContextMenuEvent.Reason.Keyboard, at, view.viewport().mapToGlobal(at)) + QApplication.sendEvent(view.viewport(), key) + + assert len(menus) == 1 + + +def test_a_pan_whose_release_went_elsewhere_stops_with_the_next_move(view, menus, menu_on_press): + # A menu opened on the press (Linux, macOS) takes the release, and the + # canvas went on following a mouse with no button held + window = view.windowHandle() + QTest.mousePress(window, Qt.MouseButton.RightButton, _NO_KEYS, _at(view, 200, 150)) + assert len(menus) == 1 + left = view.horizontalScrollBar().value() + + move = QMouseEvent( + QEvent.Type.MouseMove, QPointF(100, 150), QPointF(view.viewport().mapToGlobal(QPoint(100, 150))), + Qt.MouseButton.NoButton, Qt.MouseButton.NoButton, _NO_KEYS) + QApplication.sendEvent(view.viewport(), move) + + assert view.horizontalScrollBar().value() == left + QTest.mouseRelease(window, Qt.MouseButton.RightButton, _NO_KEYS, _at(view, 100, 150)) From 9ce87fcf6897ff2fd5b10c0cca68628bd44ac920 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 18:33:39 +0800 Subject: [PATCH 013/166] Bring the coverage figures in architecture_explore.md up to date --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 8 ++++++++ docs/updates/README.md | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 6360efd6..2210e3a8 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 - **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2293 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 -- **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/`、`tools_gui`、`dialog` 95–100%;`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%、`process_executor` 39%、`connect_gui` 28%) +- **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) - **靜態分析** SonarCloud(`sonar-project.properties`,CI-based analysis;Automatic Analysis 已關閉且必須維持關閉,兩種模式互斥)+ Codacy(`.codacy.yml`)+ Bandit(`pyproject.toml` 中排除 test、skip B101/B404) - **SonarCloud 方案限制** 該組織的方案只開放 `main` 與 PR 的分析結果。非 main 分支的分析送得出去、CE 任務也會成功,但結果讀回來是 403(組織內每個專案都只有 `main` 一條分支)。因此 `dev.yml` 只在 PR 時掃描,`stable.yml` 另外掃 push to `main` diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 3b74b7ad..8843274d 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -744,3 +744,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2293 tests in 128 files. The diagram tests (148) pass. `ruff check` and C901 clean. - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_view.py`, `test/test_utils/test_diagram_view_pan.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-283 · 2026-09-24 · architecture_explore.md coverage figures re-measured · #docs #test + +- **What**: `architecture_explore.md` §18 still said coverage was 60% overall, with `diagram_editor` 45%, `process_executor` 39% and `connect_gui` 28%. Those figures predate most of the tests written since. +- **Measured**: `pytest test/test_utils --cov=pybreeze` with `.coveragerc` (branch coverage) at 96f287e in a clean worktree. Total 89%. `utils/`, `tools_gui` and `dialog` 97-100%, `extend/` 93%, `connect_gui` 89%, `diagram_editor` 85%, `jupyter_lab_gui` 82%, `menu` 81%, `editor_main` 71%. The main window mostly runs in the startup tests' child processes, which `.coveragerc` does not measure. +- **Same run**: 3 child-IDE tests failed (`test_started_menus.py::test_a_broken_extend_tab_costs_only_itself`, `test_startup_language.py` Traditional Chinese and Japanese): a 120 s timeout, and `OpenBLAS error: Memory allocation still failed` with 560 MB of 32 GB free while other projects' processes held the memory. The plain suite at 35aaecd passed all of them (2270 passed, 15 skipped). +- **Files**: `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6fff300f..b907bd70 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-283 | 2026-09-24 | architecture_explore.md coverage figures re-measured | #docs #test | [2026-09-b](2026-09-b.md) | | U-20260924-282 | 2026-09-24 | A right-drag on the diagram canvas pans without opening the menu | #fix #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-281 | 2026-09-24 | architecture_explore.md line counts re-measured | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-280 | 2026-09-24 | curl -b '' reads no cookie file | #fix #curl | [2026-09-b](2026-09-b.md) | @@ -364,4 +365,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 80 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 81 | From 2576d92213b9efadec0f5835b0097c260ab951af Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 18:56:20 +0800 Subject: [PATCH 014/166] Use Taiwan terms in the Traditional Chinese interface --- README/README_zh-TW.md | 22 +++--- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 +++ docs/updates/README.md | 3 +- progress.md | 1 + .../extend_traditional_chinese.py | 70 +++++++++---------- test/test_utils/test_language_parity.py | 14 ++++ 7 files changed, 73 insertions(+), 48 deletions(-) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 64e9c0b8..270fc409 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -7,7 +7,7 @@ [English](../README.md) | [简体中文](README_zh-CN.md) -**PyBreeze** 是一款專為自動化工程師打造的 Python IDE。Web、API、GUI 與負載測試都在同一個視窗裡,旁邊還有自動化工作實際需要的日常 HTTP 工具——不必四處找插件,也不必挖掘環境設定。 +**PyBreeze** 是一款專為自動化工程師打造的 Python IDE。Web、API、GUI 與負載測試都在同一個視窗裡,旁邊還有自動化工作實際需要的日常 HTTP 工具——不必四處找外掛,也不必挖掘環境設定。 ![PyBreeze 主視窗](../images/main_window.png) @@ -21,7 +21,7 @@ - [四維自動化](#四維自動化) - [內建工具](#內建工具) - [AI 輔助開發](#ai-輔助開發) -- [插件系統](#插件系統) +- [外掛系統](#外掛系統) - [多語言介面](#多語言介面) - [架構](#架構) - [安裝](#安裝) @@ -193,16 +193,16 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: --- -## 插件系統 +## 外掛系統 -PyBreeze 沿用 JEditor 的插件架構,會自動從工作目錄中的 `jeditor_plugins/` 目錄探索插件。插件可以註冊: +PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jeditor_plugins/` 目錄探索外掛。外掛可以註冊: - **語法高亮** — 任何語言的關鍵字集與規則 - **介面翻譯** — 新的介面語言 - **執行設定** — 為直譯式(`go run main.go`)與編譯式(`gcc main.c -o main` 後執行)語言提供「Run with…」,透過 PyBreeze 的 `FileRunnerProcess` 執行,並在結束後清掉編譯產物 -- **插件瀏覽器** — 在 IDE 內瀏覽並安裝遠端儲存庫中的插件 +- **外掛瀏覽器** — 在 IDE 內瀏覽並安裝遠端儲存庫中的外掛 -已載入的插件會出現在它們專屬的 **Plugins** 選單下,附一個 About 項目和一個執行動作,動作名稱標示它能執行的副檔名。[PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md) 說明 PyBreeze 額外提供的部分,並連到 JEditor 的指南,那裡有完整的 API 與實作範例(C、C++、Go、Java、Rust,以及法文翻譯)。 +已載入的外掛會出現在它們專屬的 **Plugins**(外掛)選單下,附一個 About 項目和一個執行動作,動作名稱標示它能執行的副檔名。[PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md) 說明 PyBreeze 額外提供的部分,並連到 JEditor 的指南,那裡有完整的 API 與實作範例(C、C++、Go、Java、Rust,以及法文翻譯)。 --- @@ -211,7 +211,7 @@ PyBreeze 沿用 JEditor 的插件架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 733 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯插件加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 733 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 --- @@ -355,13 +355,13 @@ start_editor(theme="dark_teal.xml") # 任何 qt_material 主題 ``` PyBreeze/ ├── pybreeze/ -│ ├── __init__.py # 公開 API(start_editor、插件 re-export) +│ ├── __init__.py # 公開 API(start_editor、外掛 re-export) │ ├── __main__.py # 進入點(python -m pybreeze) │ ├── extend/ │ │ ├── process_executor/ # 子行程隔離層 │ │ │ ├── python_task_process_manager.py # TaskProcessManager(核心) │ │ │ ├── process_executor_utils.py # build_process / start_process -│ │ │ ├── file_runner_process.py # 插件執行設定(任何語言) +│ │ │ ├── file_runner_process.py # 外掛執行設定(任何語言) │ │ │ ├── queue_pump.py # 共用的管線讀取器 + QTimer 取出 │ │ │ ├── api_testka/ auto_control/ web_runner/ │ │ │ ├── load_density/ file_automation/ mail_thunder/ @@ -371,7 +371,7 @@ PyBreeze/ │ ├── extend_multi_language/ # 內建多語言(英文、繁體中文) │ ├── pybreeze_ui/ │ │ ├── editor_main/ # 主視窗 + 檔案樹右鍵選單 -│ │ ├── menu/ # Automation / Install / Tools / 插件選單 +│ │ ├── menu/ # Automation / Install / Tools / 外掛選單 │ │ ├── tools_gui/ # cURL、HAR、JWT、diff、regex 等工具分頁 │ │ ├── diagram_editor/ # 所見即所得圖表編輯器 │ │ ├── extend_ai_gui/ # CoT 審查、提示詞編輯器、skill send @@ -387,7 +387,7 @@ PyBreeze/ ├── test/ # 單元測試(test_utils)+ 啟動測試 ├── images/ # 截圖 ├── architecture_explore.md # 逐模組的架構筆記 -├── PLUGIN_GUIDE.md # 插件開發文件 +├── PLUGIN_GUIDE.md # 外掛開發文件 ├── pyproject.toml # 套件設定(穩定版) ├── dev.toml # 套件設定(開發通道) └── requirements.txt # 執行階段相依套件 diff --git a/architecture_explore.md b/architecture_explore.md index 2210e3a8..8448c7d7 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2293 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2294 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 8843274d..a020eb21 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -752,3 +752,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Same run**: 3 child-IDE tests failed (`test_started_menus.py::test_a_broken_extend_tab_costs_only_itself`, `test_startup_language.py` Traditional Chinese and Japanese): a 120 s timeout, and `OpenBLAS error: Memory allocation still failed` with 560 MB of 32 GB free while other projects' processes held the memory. The plain suite at 35aaecd passed all of them (2270 passed, 15 skipped). - **Files**: `architecture_explore.md` - **Open items**: none. + +## U-20260924-284 · 2026-09-24 · Taiwan terms in the Traditional Chinese interface · #fix #i18n + +- **What**: PyBreeze's own Traditional Chinese entries used Mainland terms. The 22 automation Run entries said 運行 (「運行 APITestka 腳本」…), while the run window said 執行. The plugin menu and plugin browser (9 entries PyBreeze defines over JEditor's) said 插件, the three SSH status lines 終端, and the diagram property panel 字體大小. Found by a term sweep over the dictionary. +- **Fix**: 運行 → 執行, 插件 → 外掛, 終端 → 終端機, 字體 → 字型, in `extend_traditional_chinese.py` (35 entries). `README/README_zh-TW.md` follows the interface: 外掛系統, 外掛瀏覽器, and the **Plugins**(外掛)menu named like the other menus. `README.md` and `README_zh-CN.md` do not name these words (the Simplified Chinese README keeps 插件, the Mainland term), so they are unchanged. +- **Tests**: `test_language_parity.py` +1 (`test_traditional_chinese_uses_taiwan_terms`: ten Mainland terms, and 終端 not followed by 機). It failed on the old dictionary. +- **Result / numbers**: 2294 tests in 128 files. The language, run-notice and error-text tests (102) pass. +- **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `README/README_zh-TW.md`, `architecture_explore.md`, `progress.md` +- **Open items**: #108 — JEditor's own entries (its Run menu's 運行, its font menus' 字體) still show inside PyBreeze; they change in JEditor. diff --git a/docs/updates/README.md b/docs/updates/README.md index b907bd70..01f9f261 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-284 | 2026-09-24 | Taiwan terms in the Traditional Chinese interface | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-283 | 2026-09-24 | architecture_explore.md coverage figures re-measured | #docs #test | [2026-09-b](2026-09-b.md) | | U-20260924-282 | 2026-09-24 | A right-drag on the diagram canvas pans without opening the menu | #fix #diagram | [2026-09-b](2026-09-b.md) | | U-20260924-281 | 2026-09-24 | architecture_explore.md line counts re-measured | #docs | [2026-09-b](2026-09-b.md) | @@ -365,4 +366,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 81 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 82 | diff --git a/progress.md b/progress.md index 07a0af4f..3e321cea 100644 --- a/progress.md +++ b/progress.md @@ -16,3 +16,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#102** [BLOCKED] A report mail can wait forever: je_mail_thunder's `SMTPWrapper(host, port)` passes no timeout to `SMTP_SSL`, so a mail server that accepts the connection and then stalls holds the report-mail thread (`extend/mail_thunder_extend/mail_thunder_setting.py`, `send_report`) with no end, and the run window never says whether the report went. Needs a `timeout` argument in MailThunder (`je_mail_thunder/smtp/smtp_wrapper.py:20`); then pass 30 s here. - **#103** [BLOCKED] PyBreeze's automation keywords are never highlighted: `syntax_extend_package` registers them for `.json`, `.yml` and `.yaml` (`pybreeze_ui/syntax/syntax_extend.py`), but JEditor has built-in rules for those suffixes, so `CodeEditor.reset_highlighter` gets a `GenericHighlighter` from `highlighter_for`, and only `PythonHighlighter` reads the plugin registry (`je_editor/pyside_ui/code/syntax/generic_syntax.py:124`, `code_edit_plaintext.py:423-425` in `D:\Codes\JEDITOR` at d372c35). Needs JEditor's generic highlighter to add a registered language's words for the suffix; the `architecture.md` §6 contract changes with it. - **#106** [DECIDE] `test_pioneer` is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), and before 0.1.34 it read a YAML file in the locale's encoding: on a Traditional Chinese Windows (cp950) TestPioneer "Run YAML" of a file with a non-ASCII character, which the editor saves as UTF-8, exits 1 with `UnicodeDecodeError` (`extend/process_executor/test_pioneer/test_pioneer_process_manager.py`). This repo's `.venv` has 0.1.30; 0.1.34 reads it as UTF-8. Should the dependencies require `test_pioneer>=0.1.34`? +- **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index af91c785..ae7c261a 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -15,7 +15,7 @@ "automation_menu_label": "自動化", "install_menu_label": "安裝", # Normal label - "run_label": "運行", + "run_label": "執行", "help_label": "幫助", "project_label": "專案", "create_project_exists": "{path} 已經存在。要用新的範本檔取代,並捨棄你對它們的修改嗎?", @@ -26,10 +26,10 @@ "tab_menu_jupyterlab_tab_name": "JupyterLab", # APITestka Menu "apitestka_menu_label": "APITestka", - "apitestka_run_script_label": "運行 APITestka 腳本", - "apitestka_run_script_with_send_label": "運行 APITestka 腳本並寄信", - "apitestka_run_multi_script_label": "運行多個 APITestka 腳本", - "apitestka_run_multi_script_with_send_label": "運行多個 APITestka 腳本並寄信", + "apitestka_run_script_label": "執行 APITestka 腳本", + "apitestka_run_script_with_send_label": "執行 APITestka 腳本並寄信", + "apitestka_run_multi_script_label": "執行多個 APITestka 腳本", + "apitestka_run_multi_script_with_send_label": "執行多個 APITestka 腳本並寄信", "apitestka_doc_label": "開啟 APITestka 文件", "apitestka_doc_tab_label": "APITestka 文件", "apitestka_github_label": "開啟 APITestka GitHub", @@ -37,10 +37,10 @@ "apitestka_create_project_label": "建立 APITestka 專案", # Autocontrol Menu "autocontrol_menu_label": "Autocontrol", - "autocontrol_run_script_label": "運行 Autocontrol 腳本", - "autocontrol_run_script_with_send_label": "運行 Autocontrol 腳本並寄信", - "autocontrol_run_multi_script_label": "運行多個 Autocontrol 腳本", - "autocontrol_run_multi_script_with_send_label": "運行多個 Autocontrol 腳本並寄信", + "autocontrol_run_script_label": "執行 Autocontrol 腳本", + "autocontrol_run_script_with_send_label": "執行 Autocontrol 腳本並寄信", + "autocontrol_run_multi_script_label": "執行多個 Autocontrol 腳本", + "autocontrol_run_multi_script_with_send_label": "執行多個 Autocontrol 腳本並寄信", "autocontrol_doc_label": "開啟 Autocontrol 文件", "autocontrol_doc_tab_label": "Autocontrol 文件", "autocontrol_github_label": "開啟 Autocontrol GitHub", @@ -53,10 +53,10 @@ "autocontrol_record_copied": "錄製結果已複製到剪貼簿:目前最前面的不是編輯分頁,沒有地方可以插入。", # File Automation Menu "file_automation_menu_label": "FileAutomation", - "file_automation_run_script_label": "運行 FileAutomation 腳本", - "file_automation_run_script_with_send_label": "運行 FileAutomation 腳本並寄信", - "file_automation_run_multi_script_label": "運行多個 FileAutomation 腳本", - "file_automation_run_multi_script_with_send_label": "運行多個 FileAutomation 腳本並寄信", + "file_automation_run_script_label": "執行 FileAutomation 腳本", + "file_automation_run_script_with_send_label": "執行 FileAutomation 腳本並寄信", + "file_automation_run_multi_script_label": "執行多個 FileAutomation 腳本", + "file_automation_run_multi_script_with_send_label": "執行多個 FileAutomation 腳本並寄信", "file_automation_doc_label": "開啟 FileAutomation 文件", "file_automation_doc_tab_label": "FileAutomation 文件", "file_automation_github_label": "開啟 FileAutomation GitHub", @@ -64,10 +64,10 @@ "file_automation_create_project_label": "建立 FileAutomation 專案", # Load Density Menu "load_density_menu_label": "LoadDensity", - "load_density_run_script_label": "運行 LoadDensity 腳本", - "load_density_run_script_with_send_label": "運行 LoadDensity 腳本並寄信", - "load_density_run_multi_script_label": "運行多個 LoadDensity 腳本", - "load_density_run_multi_script_with_send_label": "運行多個 LoadDensity 腳本並寄信", + "load_density_run_script_label": "執行 LoadDensity 腳本", + "load_density_run_script_with_send_label": "執行 LoadDensity 腳本並寄信", + "load_density_run_multi_script_label": "執行多個 LoadDensity 腳本", + "load_density_run_multi_script_with_send_label": "執行多個 LoadDensity 腳本並寄信", "load_density_doc_label": "開啟 LoadDensity 文件", "load_density_doc_tab_label": "LoadDensity 文件", "load_density_github_label": "開啟 LoadDensity GitHub", @@ -75,7 +75,7 @@ "load_density_create_project_label": "建立 LoadDensity 專案", # Mail Thunder Menu "mail_thunder_menu_label": "MailThunder", - "mail_thunder_run_script_label": "運行 MailThunder 腳本", + "mail_thunder_run_script_label": "執行 MailThunder 腳本", "mail_thunder_doc_label": "開啟 MailThunder 文件", "mail_thunder_doc_tab_label": "MailThunder 文件", "mail_thunder_github_label": "開啟 MailThunder GitHub", @@ -83,10 +83,10 @@ "mail_thunder_create_project_label": "建立 MailThunder 專案", # Webrunner Menu "web_runner_menu_label": "WebRunner", - "web_runner_run_script_label": "運行 WebRunner 腳本", - "web_runner_run_script_with_send_label": "運行 WebRunner 腳本並寄信", - "web_runner_run_multi_script_label": "運行多個 WebRunner 腳本", - "web_runner_run_multi_script_with_send_label": "運行多個 WebRunner 腳本並寄信", + "web_runner_run_script_label": "執行 WebRunner 腳本", + "web_runner_run_script_with_send_label": "執行 WebRunner 腳本並寄信", + "web_runner_run_multi_script_label": "執行多個 WebRunner 腳本", + "web_runner_run_multi_script_with_send_label": "執行多個 WebRunner 腳本並寄信", "web_runner_doc_label": "開啟 WebRunner 文件", "web_runner_doc_tab_label": "WebRunner 文件", "web_runner_github_label": "開啟 WebRunner GitHub", @@ -231,9 +231,9 @@ "unsaved_close_title": "尚未儲存的變更", "prompt_editor_reload_over_edits": "{filename} 在磁碟上被改過了。要重新載入並捨棄這裡還沒存的編輯嗎?", - "ssh_state_shell_and_files": "已連線:終端與檔案", - "ssh_state_shell_only": "已連線:只有終端(檔案樹沒連上)", - "ssh_state_files_only": "已連線:只有檔案(終端沒連上)", + "ssh_state_shell_and_files": "已連線:終端機與檔案", + "ssh_state_shell_only": "已連線:只有終端機(檔案樹沒連上)", + "ssh_state_files_only": "已連線:只有檔案(終端機沒連上)", "ssh_state_neither": "未連線", "ssh_file_viewer_message_session_busy": "SFTP 連線正在傳輸檔案或列出目錄,請等它完成後再試。", "ssh_file_viewer_dialog_title_transfer_running": "已有傳輸進行中", @@ -678,7 +678,7 @@ "diagram_editor_prop_shape": "形狀", "diagram_editor_prop_fill_color": "填充", "diagram_editor_prop_border_color": "邊框", - "diagram_editor_prop_font_size": "字體大小", + "diagram_editor_prop_font_size": "字型大小", "diagram_editor_prop_label": "標籤", "diagram_editor_prop_line_style": "樣式", "diagram_editor_prop_line_color": "顏色", @@ -737,7 +737,7 @@ "file_tree_ctx_confirm_delete": "確認刪除", "file_tree_ctx_confirm_delete_message": "確定要刪除 '{name}' 嗎?", # Plugin Menu - "plugin_menu_label": "插件", + "plugin_menu_label": "外掛", "plugin_menu_about": "關於", "plugin_menu_run_with": "以 {name} 執行", # Run with Menu @@ -747,24 +747,24 @@ "run_with_save_failed": "{file} 無法存檔,所以沒有執行:{error}", "run_with_suffix_mismatch": "目前的檔案 ({suffix}) 與預期的副檔名不符: {expected}", # Plugin Browser - "plugin_browser_tab_name": "插件瀏覽器", + "plugin_browser_tab_name": "外掛瀏覽器", "plugin_browser_repo_label": "儲存庫 URL:", - "plugin_browser_fetch_btn": "取得插件", - "plugin_browser_col_name": "插件", + "plugin_browser_fetch_btn": "取得外掛", + "plugin_browser_col_name": "外掛", "plugin_browser_col_path": "路徑", "plugin_browser_col_size": "大小", - "plugin_browser_select_hint": "選擇插件以檢視詳細資訊", + "plugin_browser_select_hint": "選擇外掛以檢視詳細資訊", "plugin_browser_download_btn": "下載並安裝", "plugin_browser_loading_source": "正在載入原始碼...", - "plugin_browser_overwrite_title": "插件已存在", + "plugin_browser_overwrite_title": "外掛已存在", "plugin_browser_overwrite_msg": "{name} 已存在,是否覆蓋?", "plugin_browser_invalid_url": "無效的儲存庫 URL", "plugin_browser_status_ready": "就緒", - "plugin_browser_status_fetching": "正在取得插件列表...", - "plugin_browser_status_loaded": "已載入 {count} 個插件", + "plugin_browser_status_fetching": "正在取得外掛列表...", + "plugin_browser_status_loaded": "已載入 {count} 個外掛", "plugin_browser_status_downloading": "正在下載 {name}...", "plugin_browser_status_installed": "已安裝:{path}", - "plugin_browser_restart_hint": "插件已下載至:\n{path}\n\n請重新啟動編輯器以啟用。", + "plugin_browser_restart_hint": "外掛已下載至:\n{path}\n\n請重新啟動編輯器以啟用。", # 工具拒絕輸入的原因(pybreeze_ui/error_text.py):exception_tags 各常數的翻譯 "error_text_cant_reformat_json_error": "無法重新格式化 JSON:型別正確嗎?", "error_text_wrong_json_data_error": "無法解析 JSON", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 43191802..009fa311 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -68,6 +68,20 @@ def test_no_word_is_written_twice(self): } assert not doubled, f"A word written twice: {doubled}" + def test_traditional_chinese_uses_taiwan_terms(self): + # The automation menus said 運行 where the run window said 執行; JEditor's + # own entries (its Run and plugin menus) are not in this dictionary + mainland = {"運行": "執行", "字體": "字型", "插件": "外掛", "默認": "預設", "文件夾": "資料夾", + "信息": "訊息", "軟件": "軟體", "數據": "資料", "屏幕": "螢幕", "鼠標": "滑鼠"} + found = { + key: [word for word in mainland if word in value] + for key, value in ZH.items() + if any(word in value for word in mainland) + } + assert not found, f"Mainland terms, Taiwan uses {mainland}: {found}" + # 終端 on its own is 終端機 + assert not {key: value for key, value in ZH.items() if re.search("終端(?!機)", value)} + class TestCodeKeysAreDefined: def test_every_get_key_exists_in_dict(self): From 6ee887b40a783acd72a49d40858aaaca40706834 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:00:16 +0800 Subject: [PATCH 015/166] Send Enter for an empty line on the SSH command line --- architecture_explore.md | 4 ++-- docs/updates/2026-09-b.md | 9 +++++++ docs/updates/README.md | 3 ++- .../connect_gui/ssh/ssh_command_widget.py | 10 +++++--- test/test_utils/test_ssh_terminal_output.py | 24 +++++++++++++++++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 8448c7d7..e9b4a791 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2294 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2296 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index a020eb21..d84f6131 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -761,3 +761,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2294 tests in 128 files. The language, run-notice and error-text tests (102) pass. - **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `README/README_zh-TW.md`, `architecture_explore.md`, `progress.md` - **Open items**: #108 — JEditor's own entries (its Run menu's 運行, its font menus' 字體) still show inside PyBreeze; they change in JEditor. + +## U-20260924-285 · 2026-09-24 · Enter on an empty SSH command line sends Enter · #fix #ssh + +- **What**: the SSH tab's command line sent nothing when Enter was pressed with nothing typed (`SSHCommandWidget.send_command` returned on an empty line). A shell prompt that waits for Enter alone could not be answered: a default like `[Y/n]`, "Press Enter to continue", a pager. +- **Fix**: with a session, an empty line is sent as a bare newline. Without one, an empty line still does nothing, and a typed line still says to connect first. +- **Tests**: `test_ssh_terminal_output.py` +2 (an empty line sends `\n`; without a session it shows no dialog). The first failed on the old code. +- **Result / numbers**: 2296 tests in 128 files. The SSH tests (106) pass. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 01f9f261..cd839d79 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-285 | 2026-09-24 | Enter on an empty SSH command line sends Enter | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-284 | 2026-09-24 | Taiwan terms in the Traditional Chinese interface | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-283 | 2026-09-24 | architecture_explore.md coverage figures re-measured | #docs #test | [2026-09-b](2026-09-b.md) | | U-20260924-282 | 2026-09-24 | A right-drag on the diagram canvas pans without opening the menu | #fix #diagram | [2026-09-b](2026-09-b.md) | @@ -366,4 +367,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 82 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 83 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index f3bc5a8b..9b1f30f7 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -377,16 +377,20 @@ def _on_closed(self, msg: str): self.state_changed.emit() def send_command(self): + """Send the typed line and a newline to the shell. + + An empty line is sent too, as Enter alone: a prompt's default + (``[Y/n]``, "Press Enter to continue") is taken that way. Without a + session it only asks to connect when something was typed. + """ cmd = self.command_input_edit.text() - if not cmd: - return if self.shell_channel and not self.shell_channel.closed: try: send_all(self.shell_channel, (cmd + "\n").encode("utf-8")) self.command_input_edit.clear() except (OSError, paramiko.SSHException) as e: self.append_text(f"{self.word_dict.get('ssh_command_widget_error_message_send_failed')} {e}\n") - else: + elif cmd: QMessageBox.information( self, self.word_dict.get('ssh_command_widget_dialog_title_not_connected'), diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index 3f1a9eee..6a2eedbe 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -10,6 +10,7 @@ from PySide6.QtWidgets import QApplication from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.connect_gui.ssh import ssh_command_widget from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import ( SSHCommandWidget, SSHReaderThread, @@ -194,6 +195,29 @@ def test_a_long_command_arrives_whole_as_utf8(self, app): widget.shell_channel = None widget.close() + def test_an_empty_line_sends_enter(self, app): + # It sent nothing, so a prompt's default ("[Y/n]", "Press Enter to + # continue") could not be taken + widget = SSHCommandWidget() + channel = PartialSendChannel() + widget.shell_channel = channel + + widget.send_command() + + assert channel.sent == [b"\n"] + widget.shell_channel = None + widget.close() + + def test_an_empty_line_without_a_session_asks_nothing(self, app, monkeypatch): + widget = SSHCommandWidget() + asked: list = [] + monkeypatch.setattr(ssh_command_widget.QMessageBox, "information", lambda *args: asked.append(args)) + + widget.send_command() + + assert asked == [] + widget.close() + class _IdleReader: """A reader that is never started: the connect message is all that is looked at.""" From f1c4ebe3fc56b5a00f81e2dc517cbf82dd4a53d1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:05:38 +0800 Subject: [PATCH 016/166] Interrupt what runs in the SSH shell --- README.md | 4 +- README/README_zh-CN.md | 4 +- README/README_zh-TW.md | 4 +- architecture_explore.md | 10 ++-- docs/updates/2026-09-b.md | 10 ++++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 4 ++ .../extend_traditional_chinese.py | 2 + .../connect_gui/ssh/ssh_command_widget.py | 48 ++++++++++++++--- test/test_utils/test_ssh_terminal_output.py | 52 +++++++++++++++++++ 10 files changed, 123 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index df49567a..548e540d 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive, and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive (Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 733 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 735 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 53d89de4..e6a9b55e 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell,以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell(空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 @@ -211,7 +211,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 733 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 735 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 270fc409..e2c9a84e 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell,以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell(空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 @@ -211,7 +211,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 733 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 735 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index e9b4a791..dfa86089 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -50,7 +50,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ┌─────────────────────────────────────────────────────────────────────────┐ │ 基礎層 Foundation │ │ pybreeze/utils/ 18 個工具子套件(純邏輯,可單測) │ - │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 733 鍵) │ + │ pybreeze/extend_multi_language/ 內建 i18n(英 / 繁中,各 735 鍵) │ └─────────────────────────────────────────────────────────────────────────┘ ▼ 外部子行程:python -m je_api_testka / je_auto_control / je_web_runner / @@ -66,7 +66,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 733 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 735 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示 @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -363,7 +363,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 733 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 735 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2296 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2300 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index d84f6131..8885308d 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -770,3 +770,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2296 tests in 128 files. The SSH tests (106) pass. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-286 · 2026-09-24 · Interrupt what runs in the SSH shell · #feature #ssh #i18n + +- **What**: the SSH tab had no way to send Ctrl+C. The command line sends whole lines, so a `ping`, `tail -f` or `top` started there ran until the session was disconnected. +- **Change**: an **Interrupt** button (中斷) beside Send, and Ctrl+C in the command line when no text is selected, send `\x03` (`SSHCommandWidget.send_interrupt`), which the remote terminal turns into SIGINT. With text selected, Ctrl+C copies as before (`eventFilter`). What is typed in the command line stays. Without a session it does nothing. `send_command` and `send_interrupt` share `_send`, which reports a failed send in the terminal. +- **Tests**: `test_ssh_terminal_output.py` +4 (the button sends `\x03` and keeps the line; Ctrl+C in the command line interrupts; Ctrl+C on a selection copies and sends nothing; the button without a session does nothing). +- **Docs**: the SSH paragraph of `README.md`, `README/README_zh-TW.md` and `README/README_zh-CN.md` names Interrupt, Ctrl+C and Enter on an empty line. Key count 733 → 735 there and in `architecture_explore.md`, whose SSH row describes the interrupt. +- **Result / numbers**: 735 keys in each language. 2300 tests in 128 files. The SSH, language and started-menu tests (123) pass. Full suite at 2576d92 (before this change): 2279 passed, 15 skipped; both startup tests exit 0. `ruff check` and C901 clean. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index cd839d79..8bf7eee5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-286 | 2026-09-24 | Interrupt what runs in the SSH shell | #feature #ssh #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-285 | 2026-09-24 | Enter on an empty SSH command line sends Enter | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-284 | 2026-09-24 | Taiwan terms in the Traditional Chinese interface | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-283 | 2026-09-24 | architecture_explore.md coverage figures re-measured | #docs #test | [2026-09-b](2026-09-b.md) | @@ -367,4 +368,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 83 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 84 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index f261e5e6..0e840aaa 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -169,6 +169,10 @@ # SSH command widget "ssh_command_widget_window_title_ssh_command_widget": "SSH Command Widget", "ssh_command_widget_button_label_send_command": "Send", + "ssh_command_widget_button_label_interrupt": "Interrupt", + "ssh_command_widget_tooltip_interrupt": + "Send Ctrl+C to stop what runs in the shell (Ctrl+C in the command line does the same " + "when no text is selected)", "ssh_command_widget_input_placeholder_command_line": "Type command then Enter...", "ssh_command_widget_dialog_title_input_error": "Input error", "ssh_command_widget_dialog_message_input_error_host_user_required": "Host and username are required.", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index ae7c261a..5204e0da 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -164,6 +164,8 @@ # SSH command widget "ssh_command_widget_window_title_ssh_command_widget": "SSH 指令介面", "ssh_command_widget_button_label_send_command": "送出", + "ssh_command_widget_button_label_interrupt": "中斷", + "ssh_command_widget_tooltip_interrupt": "送出 Ctrl+C,停止 shell 中正在執行的程式(指令列沒有選取文字時按 Ctrl+C 也一樣)", "ssh_command_widget_input_placeholder_command_line": "輸入指令後按 Enter...", "ssh_command_widget_dialog_title_input_error": "輸入錯誤", "ssh_command_widget_dialog_message_input_error_host_user_required": "必須輸入主機與使用者名稱。", diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 9b1f30f7..4a0ac280 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -5,7 +5,7 @@ import weakref import paramiko -from PySide6.QtCore import QThread, Signal +from PySide6.QtCore import QEvent, QThread, Qt, Signal from PySide6.QtGui import QTextCursor from PySide6.QtWidgets import ( QWidget, QLineEdit, QPushButton, @@ -70,6 +70,9 @@ def feed(self, data: bytes) -> str: SSH_KEEPALIVE_SECONDS = 30 +# What Ctrl+C sends in a terminal (ETX): the shell's line discipline turns it into SIGINT +INTERRUPT = b"\x03" + # Longest a command waits for the server to take it (a full SSH window), on the UI thread SEND_TIMEOUT_SECONDS = 5 @@ -190,6 +193,9 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: self.command_input_edit = QLineEdit() self.command_send_button = QPushButton( self.word_dict.get("ssh_command_widget_button_label_send_command")) + self.interrupt_button = QPushButton( + self.word_dict.get("ssh_command_widget_button_label_interrupt")) + self.interrupt_button.setToolTip(self.word_dict.get("ssh_command_widget_tooltip_interrupt")) self._setup_ui() self._bind_events() @@ -207,6 +213,7 @@ def _setup_ui(self): command_input_bar = QHBoxLayout() command_input_bar.addWidget(self.command_input_edit) command_input_bar.addWidget(self.command_send_button) + command_input_bar.addWidget(self.interrupt_button) main_widget = QVBoxLayout() main_widget.addWidget(self.login_widget) # 插入登入介面 @@ -223,6 +230,18 @@ def _bind_events(self): # 綁定其他按鈕 self.command_send_button.clicked.connect(self.send_command) self.command_input_edit.returnPressed.connect(self.send_command) + self.interrupt_button.clicked.connect(self.send_interrupt) + self.command_input_edit.installEventFilter(self) + + def eventFilter(self, watched, event) -> bool: + """Ctrl+C in the command line interrupts the shell, unless it copies a selection.""" + if (watched is self.command_input_edit and event.type() == QEvent.Type.KeyPress + and event.key() == Qt.Key.Key_C + and event.modifiers() == Qt.KeyboardModifier.ControlModifier + and not self.command_input_edit.hasSelectedText()): + self.send_interrupt() + return True + return super().eventFilter(watched, event) def append_text(self, text: str): """Add a notice of our own, starting on a line of its own.""" @@ -384,18 +403,35 @@ def send_command(self): session it only asks to connect when something was typed. """ cmd = self.command_input_edit.text() - if self.shell_channel and not self.shell_channel.closed: - try: - send_all(self.shell_channel, (cmd + "\n").encode("utf-8")) + if self._has_shell(): + if self._send((cmd + "\n").encode("utf-8")): self.command_input_edit.clear() - except (OSError, paramiko.SSHException) as e: - self.append_text(f"{self.word_dict.get('ssh_command_widget_error_message_send_failed')} {e}\n") elif cmd: QMessageBox.information( self, self.word_dict.get('ssh_command_widget_dialog_title_not_connected'), self.word_dict.get('ssh_command_widget_dialog_message_not_connected_shell')) + def send_interrupt(self) -> None: + """Send Ctrl+C to the shell, which stops what runs in it (``ping``, ``tail -f``). + + What is typed in the command line stays. Without a session it does nothing. + """ + if self._has_shell(): + self._send(INTERRUPT) + + def _has_shell(self) -> bool: + return self.shell_channel is not None and not self.shell_channel.closed + + def _send(self, data: bytes) -> bool: + """Send *data* to the shell; say so in the terminal and return False when it fails.""" + try: + send_all(self.shell_channel, data) + except (OSError, paramiko.SSHException) as e: + self.append_text(f"{self.word_dict.get('ssh_command_widget_error_message_send_failed')} {e}\n") + return False + return True + def disconnect_ssh(self): self.append_text(f"{self.word_dict.get('ssh_command_widget_log_message_disconnect_in_progress')} \n") self._cleanup() diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index 6a2eedbe..bd04cb7c 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -7,6 +7,8 @@ import paramiko import pytest +from PySide6.QtCore import Qt +from PySide6.QtTest import QTest from PySide6.QtWidgets import QApplication from pybreeze.extend_multi_language.update_language_dict import update_language_dict @@ -208,6 +210,56 @@ def test_an_empty_line_sends_enter(self, app): widget.shell_channel = None widget.close() + def test_interrupt_sends_ctrl_c_and_keeps_the_line(self, app): + # A ping or tail -f could only be stopped by disconnecting + widget = SSHCommandWidget() + channel = PartialSendChannel() + widget.shell_channel = channel + widget.command_input_edit.setText("half typed") + + widget.interrupt_button.click() + + assert channel.sent == [b"\x03"] + assert widget.command_input_edit.text() == "half typed" + widget.shell_channel = None + widget.close() + + def test_ctrl_c_in_the_command_line_interrupts(self, app): + widget = SSHCommandWidget() + channel = PartialSendChannel() + widget.shell_channel = channel + widget.command_input_edit.setText("ping example.com") + + QTest.keyClick(widget.command_input_edit, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier) + + assert channel.sent == [b"\x03"] + widget.shell_channel = None + widget.close() + + def test_ctrl_c_on_a_selection_copies_it(self, app): + widget = SSHCommandWidget() + channel = PartialSendChannel() + widget.shell_channel = channel + widget.command_input_edit.setText("copy me") + widget.command_input_edit.selectAll() + + QTest.keyClick(widget.command_input_edit, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier) + + assert channel.sent == [] + assert QApplication.clipboard().text() == "copy me" + widget.shell_channel = None + widget.close() + + def test_interrupt_without_a_session_does_nothing(self, app, monkeypatch): + widget = SSHCommandWidget() + asked: list = [] + monkeypatch.setattr(ssh_command_widget.QMessageBox, "information", lambda *args: asked.append(args)) + + widget.interrupt_button.click() + + assert asked == [] + widget.close() + def test_an_empty_line_without_a_session_asks_nothing(self, app, monkeypatch): widget = SSHCommandWidget() asked: list = [] From 531ea9dc8284a3e72364f017f2f6c419dbaad66b Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:11:06 +0800 Subject: [PATCH 017/166] Remember the lines sent from the SSH command line --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 +- docs/updates/2026-09-b.md | 10 +++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_command_widget.py | 72 +++++++++++++++++-- test/test_utils/test_ssh_terminal_output.py | 69 ++++++++++++++++++ 8 files changed, 154 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 548e540d..d0d5e841 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive (Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index e6a9b55e..64016db3 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell(空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index e2c9a84e..d27a9457 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell(空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture_explore.md b/architecture_explore.md index dfa86089..131683d3 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2300 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2307 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 8885308d..8abe5787 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -780,3 +780,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 735 keys in each language. 2300 tests in 128 files. The SSH, language and started-menu tests (123) pass. Full suite at 2576d92 (before this change): 2279 passed, 15 skipped; both startup tests exit 0. `ruff check` and C901 clean. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-287 · 2026-09-24 · Command history on the SSH command line · #feature #ssh + +- **What**: the SSH tab's command line forgot each line once it was sent. Up and Down did nothing, so running a command again, or fixing a typo in one, meant typing it again. +- **Change**: `CommandHistory` (in `ssh_command_widget.py`, no Qt) remembers the lines sent: up to 500, and a line sent twice in a row only once. In the command line, Up and Down walk through them. Walking down past the newest gives back what was being typed. Sending goes back to a new line. The keys are read in `SSHCommandWidget._command_line_key`, which the command line's event filter calls. Ctrl+C is handled there too. +- **Tests**: `test_ssh_terminal_output.py` +7 (six on `CommandHistory`: back and forth, the draft, nothing sent, empty and repeated lines, the limit, sending resets the walk; one on Up and Down in the widget). +- **Docs**: the SSH paragraph of `README.md`, `README/README_zh-TW.md` and `README/README_zh-CN.md`, and `architecture_explore.md`'s SSH row. +- **Result / numbers**: 2307 tests in 128 files. The SSH terminal tests (37) pass. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8bf7eee5..231bf0f5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-287 | 2026-09-24 | Command history on the SSH command line | #feature #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-286 | 2026-09-24 | Interrupt what runs in the SSH shell | #feature #ssh #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-285 | 2026-09-24 | Enter on an empty SSH command line sends Enter | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-284 | 2026-09-24 | Taiwan terms in the Traditional Chinese interface | #fix #i18n | [2026-09-b](2026-09-b.md) | @@ -368,4 +369,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 84 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 85 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 4a0ac280..2d79b2c1 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -73,6 +73,48 @@ def feed(self, data: bytes) -> str: # What Ctrl+C sends in a terminal (ETX): the shell's line discipline turns it into SIGINT INTERRUPT = b"\x03" +# Lines the command line remembers for Up and Down; the oldest go first +HISTORY_LIMIT = 500 + + +class CommandHistory: + """Lines sent from the command line, for Up and Down to bring back, as a shell does. + + Walking up from a line being typed keeps it: walking down past the newest + line gives it back. A line sent twice in a row is remembered once. + """ + + def __init__(self, limit: int = HISTORY_LIMIT) -> None: + self._lines: list[str] = [] + self._limit = limit + self._index = 0 # len(self._lines): at the line being typed + self._draft = "" + + def add(self, line: str) -> None: + """Remember *line* (not an empty one) and go back to a new line.""" + if line and (not self._lines or self._lines[-1] != line): + self._lines.append(line) + del self._lines[:-self._limit] + self._index = len(self._lines) + self._draft = "" + + def older(self, current: str) -> str | None: + """The line before the one shown, or ``None`` at the oldest. *current* is what is typed.""" + if self._index == 0: + return None + if self._index == len(self._lines): + self._draft = current + self._index -= 1 + return self._lines[self._index] + + def newer(self) -> str | None: + """The line after the one shown, the draft after the newest, or ``None`` at the draft.""" + if self._index >= len(self._lines): + return None + self._index += 1 + return self._draft if self._index == len(self._lines) else self._lines[self._index] + + # Longest a command waits for the server to take it (a full SSH window), on the UI thread SEND_TIMEOUT_SECONDS = 5 @@ -176,6 +218,8 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: self._decoder = TerminalDecoder() # The connect in progress, if any / 正在進行的連線 self._connecting: SshConnectThread | None = None + # Lines sent, for Up and Down / 送出過的指令 + self._history = CommandHistory() host_key_asker() # built here, on the UI thread, for a connect to ask through if self.add_login_widget: @@ -234,14 +278,33 @@ def _bind_events(self): self.command_input_edit.installEventFilter(self) def eventFilter(self, watched, event) -> bool: - """Ctrl+C in the command line interrupts the shell, unless it copies a selection.""" + """Keys the command line gives to the shell rather than to its own text.""" if (watched is self.command_input_edit and event.type() == QEvent.Type.KeyPress - and event.key() == Qt.Key.Key_C - and event.modifiers() == Qt.KeyboardModifier.ControlModifier + and self._command_line_key(event.key(), event.modifiers())): + return True + return super().eventFilter(watched, event) + + def _command_line_key(self, key: int, modifiers) -> bool: + """Act on *key* in the command line; return whether it was taken. + + Ctrl+C interrupts the shell, unless it copies a selection. Up and Down + walk through the lines sent before. + """ + if (key == Qt.Key.Key_C and modifiers == Qt.KeyboardModifier.ControlModifier and not self.command_input_edit.hasSelectedText()): self.send_interrupt() return True - return super().eventFilter(watched, event) + if modifiers & ~Qt.KeyboardModifier.KeypadModifier: + return False + if key == Qt.Key.Key_Up: + line = self._history.older(self.command_input_edit.text()) + elif key == Qt.Key.Key_Down: + line = self._history.newer() + else: + return False + if line is not None: + self.command_input_edit.setText(line) + return True def append_text(self, text: str): """Add a notice of our own, starting on a line of its own.""" @@ -405,6 +468,7 @@ def send_command(self): cmd = self.command_input_edit.text() if self._has_shell(): if self._send((cmd + "\n").encode("utf-8")): + self._history.add(cmd) self.command_input_edit.clear() elif cmd: QMessageBox.information( diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index bd04cb7c..b1f4a252 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -14,6 +14,7 @@ from pybreeze.extend_multi_language.update_language_dict import update_language_dict from pybreeze.pybreeze_ui.connect_gui.ssh import ssh_command_widget from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import ( + CommandHistory, SSHCommandWidget, SSHReaderThread, TerminalDecoder, @@ -250,6 +251,25 @@ def test_ctrl_c_on_a_selection_copies_it(self, app): widget.shell_channel = None widget.close() + def test_up_and_down_bring_back_what_was_sent(self, app): + widget = SSHCommandWidget() + widget.shell_channel = PartialSendChannel() + line = widget.command_input_edit + for command in ("ls", "pwd"): + line.setText(command) + widget.send_command() + line.setText("half") + + QTest.keyClick(line, Qt.Key.Key_Up) + assert line.text() == "pwd" + QTest.keyClick(line, Qt.Key.Key_Up) + assert line.text() == "ls" + QTest.keyClick(line, Qt.Key.Key_Down) + QTest.keyClick(line, Qt.Key.Key_Down) + assert line.text() == "half" + widget.shell_channel = None + widget.close() + def test_interrupt_without_a_session_does_nothing(self, app, monkeypatch): widget = SSHCommandWidget() asked: list = [] @@ -303,3 +323,52 @@ def test_names_the_session_in_the_ide_language(self, app, monkeypatch, host, sho assert widget.login_widget.status_label.text() == "已連線" widget.shell_channel = widget.reader_thread = None widget.close() + + +class TestCommandHistory: + def _history(self, *lines: str) -> CommandHistory: + history = CommandHistory(limit=3) + for line in lines: + history.add(line) + return history + + def test_up_goes_back_and_stops_at_the_oldest(self): + history = self._history("a", "b") + + assert history.older("") == "b" + assert history.older("") == "a" + assert history.older("") is None + + def test_down_past_the_newest_gives_back_what_was_typed(self): + history = self._history("a", "b") + history.older("typing") + + assert history.newer() == "typing" + assert history.newer() is None + + def test_nothing_sent_leaves_nothing_to_walk(self): + history = self._history() + + assert history.older("x") is None + assert history.newer() is None + + def test_empty_lines_and_a_repeat_are_not_remembered(self): + history = self._history("a", "", "a") + + assert history.older("") == "a" + assert history.older("") is None + + def test_only_the_newest_lines_are_kept(self): + history = self._history("1", "2", "3", "4") + + assert [history.older(""), history.older(""), history.older(""), history.older("")] == ["4", "3", "2", None] + + def test_sending_goes_back_to_a_new_line(self): + history = self._history("a", "b") + history.older("") + history.older("") + + history.add("a") # sent again from the history + + assert history.older("") == "a" + assert history.older("") == "b" From 322e9e6c82a981be5d40fe1a04a572fb977b2cd2 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:21:36 +0800 Subject: [PATCH 018/166] Refactor: move the run window's line rewinding to terminal_view --- CLAUDE.md | 1 + architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 ++++++ docs/updates/README.md | 3 +- .../show_code_window/code_window.py | 24 ++-------------- pybreeze/pybreeze_ui/terminal_view.py | 28 +++++++++++++++++++ 6 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 pybreeze/pybreeze_ui/terminal_view.py diff --git a/CLAUDE.md b/CLAUDE.md index 3161388d..0dee2a59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,7 @@ pybreeze/ │ ├── gui_thread_gc.py # Garbage collected on a GUI-thread timer, never on a worker │ ├── plain_text.py # as_text: server/file text shown in message boxes as text, not markup │ ├── exact_text.py # exact_text: a text box read as typed (toPlainText changes U+00A0, U+2028) +│ ├── terminal_view.py # insert_rewinding: terminal output into a text view, a lone \r rewinding the line │ ├── error_text.py # error_text: a tool's English error (exception_tags) in the IDE language │ ├── closing.py # may_close / AskingDock: tabs and docks with unsaved work are asked first │ ├── dialog/ # prthinker settings dialog diff --git a/architecture_explore.md b/architecture_explore.md index 131683d3..21f72dd3 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -160,7 +160,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `ReaderGrace` / `any_alive()` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 - 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …`、`Task exit with code …`、行程仍握著輸出的註明,共 15 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 - 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 -- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`_insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 +- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 --- diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 8abe5787..9b2e2d97 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -790,3 +790,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2307 tests in 128 files. The SSH terminal tests (37) pass. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-288 · 2026-09-24 · Refactor: the run window's line rewinding in a shared module · #refactor + +- **What**: `_insert_rewinding` (a lone `\r` goes back to the line's start, as in a terminal) lived in `show_code_window/code_window.py`. The SSH terminal needs the same thing next. +- **Change**: moved as it is to `pybreeze/pybreeze_ui/terminal_view.py` as `insert_rewinding`. The run window imports it. No behaviour changes. +- **Tests**: the run-window and terminal tests (89) pass unchanged. +- **Docs**: `CLAUDE.md` architecture tree, `architecture_explore.md` (the run window's row names the new place). +- **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/show_code_window/code_window.py`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 231bf0f5..1046cd3d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-288 | 2026-09-24 | Refactor: the run window's line rewinding in a shared module | #refactor | [2026-09-b](2026-09-b.md) | | U-20260924-287 | 2026-09-24 | Command history on the SSH command line | #feature #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-286 | 2026-09-24 | Interrupt what runs in the SSH shell | #feature #ssh #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-285 | 2026-09-24 | Enter on an empty SSH command line sends Enter | #fix #ssh | [2026-09-b](2026-09-b.md) | @@ -369,4 +370,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 85 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 86 | diff --git a/pybreeze/pybreeze_ui/show_code_window/code_window.py b/pybreeze/pybreeze_ui/show_code_window/code_window.py index f0b8ddc8..3f854b85 100644 --- a/pybreeze/pybreeze_ui/show_code_window/code_window.py +++ b/pybreeze/pybreeze_ui/show_code_window/code_window.py @@ -8,6 +8,7 @@ from PySide6.QtGui import QGuiApplication, QTextCharFormat, QTextCursor from PySide6.QtWidgets import QWidget, QGridLayout, QHBoxLayout, QPlainTextEdit, QPushButton, QScrollArea +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding from pybreeze.utils.terminal_text import strip_terminal_controls, take_leading_backspaces if TYPE_CHECKING: @@ -23,27 +24,6 @@ MAX_OUTPUT_BLOCKS = 10000 -def _insert_rewinding(cursor: QTextCursor, text: str, text_format: QTextCharFormat) -> bool: - """Insert *text* at *cursor*, a lone ``\\r`` going back to the start of the line. - - As a terminal does: a progress bar that rewinds with ``\\r`` redraws its - line instead of adding one per step. ``\\r\\n`` and ``\\n`` are line breaks. - - :return: whether *text* ended on a ``\\r`` still to be applied: it waits - for what comes next, since rewound now, a finished progress bar's last - line would be erased with nothing to replace it - """ - pieces = text.replace("\r\n", "\n").split("\r") - cursor.insertText(pieces[0], text_format) - for index, piece in enumerate(pieces[1:], start=1): - if not piece and index == len(pieces) - 1: - return True - cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock, QTextCursor.MoveMode.KeepAnchor) - cursor.removeSelectedText() - cursor.insertText(piece, text_format) - return False - - class CodeWindow(QWidget): # Emitted when a window closes with nothing left running in it, so the @@ -169,6 +149,6 @@ def append_output(self, text: str, is_error: bool = False, *, own_line: bool = F cursor.movePosition(QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, min(backspaces, cursor.positionInBlock())) cursor.removeSelectedText() - self._rewind_pending = _insert_rewinding(cursor, text, text_format) + self._rewind_pending = insert_rewinding(cursor, text, text_format) if follow_output: scroll_bar.setValue(scroll_bar.maximum()) diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py new file mode 100644 index 00000000..bd57c475 --- /dev/null +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -0,0 +1,28 @@ +"""Terminal output written into a text view the way a terminal shows it. + +Shared by the run window and the SSH terminal. +""" +from __future__ import annotations + +from PySide6.QtGui import QTextCharFormat, QTextCursor + + +def insert_rewinding(cursor: QTextCursor, text: str, text_format: QTextCharFormat) -> bool: + """Insert *text* at *cursor*, a lone ``\\r`` going back to the start of the line. + + As a terminal does: a progress bar that rewinds with ``\\r`` redraws its + line instead of adding one per step. ``\\r\\n`` and ``\\n`` are line breaks. + + :return: whether *text* ended on a ``\\r`` still to be applied: it waits + for what comes next, since rewound now, a finished progress bar's last + line would be erased with nothing to replace it + """ + pieces = text.replace("\r\n", "\n").split("\r") + cursor.insertText(pieces[0], text_format) + for index, piece in enumerate(pieces[1:], start=1): + if not piece and index == len(pieces) - 1: + return True + cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock, QTextCursor.MoveMode.KeepAnchor) + cursor.removeSelectedText() + cursor.insertText(piece, text_format) + return False From be8f31943c2bab86e58811c179570e98db7e212c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:24:37 +0800 Subject: [PATCH 019/166] Redraw a progress bar's line in the SSH terminal --- architecture_explore.md | 4 ++-- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- .../connect_gui/ssh/ssh_command_widget.py | 7 +++++-- test/test_utils/test_ssh_terminal_output.py | 18 ++++++++++++++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 21f72dd3..e2b154b2 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2307 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2309 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 9b2e2d97..c143101c 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -799,3 +799,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `CLAUDE.md` architecture tree, `architecture_explore.md` (the run window's row names the new place). - **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/show_code_window/code_window.py`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-289 · 2026-09-24 · A progress bar in the SSH terminal redraws its line · #fix #ssh + +- **What**: in the SSH terminal a lone `\r` became a line break. A progress bar that redraws its line (pip, wget, curl, apt) added a line per step and pushed real output out of the 10,000-line scrollback. The run window already rewound the line. Found comparing the two views, which share `utils/terminal_text`. +- **Fix**: `SSHCommandWidget._insert_output` writes through `terminal_view.insert_rewinding` (moved there by U-20260924-288), so a lone `\r` goes back to the line's start and what follows replaces it. `TerminalDecoder` already holds a `\r` that ends a read until the next read shows whether `\n` follows. +- **Tests**: `test_ssh_terminal_output.py` +2 (a bar over several reads; a bar with `\x1b[K` in one read). Both failed on the old code. +- **Result / numbers**: 2309 tests in 128 files. The SSH tests (119) pass. Full suite at 531ea9d: 2292 passed, 15 skipped; both startup tests exit 0. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 1046cd3d..a1284a58 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-289 | 2026-09-24 | A progress bar in the SSH terminal redraws its line | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-288 | 2026-09-24 | Refactor: the run window's line rewinding in a shared module | #refactor | [2026-09-b](2026-09-b.md) | | U-20260924-287 | 2026-09-24 | Command history on the SSH command line | #feature #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-286 | 2026-09-24 | Interrupt what runs in the SSH shell | #feature #ssh #i18n | [2026-09-b](2026-09-b.md) | @@ -370,4 +371,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 86 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 87 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 2d79b2c1..ac564da2 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -6,7 +6,7 @@ import paramiko from PySide6.QtCore import QEvent, QThread, Qt, Signal -from PySide6.QtGui import QTextCursor +from PySide6.QtGui import QTextCharFormat, QTextCursor from PySide6.QtWidgets import ( QWidget, QLineEdit, QPushButton, QPlainTextEdit, QHBoxLayout, QVBoxLayout, @@ -22,6 +22,7 @@ ) from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import load_private_key, unloadable_key_reason from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_login_widget import LoginWidget +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding from pybreeze.pybreeze_ui.thread_keeper import if_alive, let_run_out from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.utils.logging.logger import pybreeze_logger @@ -329,7 +330,9 @@ def _insert_output(self, text: str) -> None: end.movePosition(QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, min(backspaces, end.positionInBlock())) end.removeSelectedText() - end.insertText(text) + # A lone \r redraws the line (a progress bar); the decoder holds one + # that ends a read until the next shows whether "\n" follows + insert_rewinding(end, text, QTextCharFormat()) if following: scroll_bar.setValue(scroll_bar.maximum()) diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index b1f4a252..f7df8d54 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -109,6 +109,24 @@ def test_a_line_ending_cut_between_reads_is_one_line_break(self, app): assert widget.terminal.toPlainText() == "line1\nline2\n" widget.close() + def test_a_progress_bar_redraws_its_line(self, app): + # Each "\r" was a line break: one line per step of pip's or wget's bar + widget = self._widget() + + for chunk in (b"start\r\n", b" 10%\r", b" 20%\r", b"100%\r\n", b"done\r\n"): + widget._on_data(chunk) + + assert widget.terminal.toPlainText() == "start\n100%\ndone\n" + widget.close() + + def test_a_rewind_and_its_text_in_one_read_redraw_the_line(self, app): + widget = self._widget() + + widget._on_data(b"a 10%\r a 20%\r\x1b[Ka100%\r\ndone\r\n") + + assert widget.terminal.toPlainText() == "a100%\ndone\n" + widget.close() + def test_a_notice_starts_on_a_line_of_its_own(self, app): widget = self._widget() widget._on_data(b"$ ") From ed451a1dd95533ac47c83f732ddcb616ebfb2e52 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:29:32 +0800 Subject: [PATCH 020/166] Show terminal output in a fixed-pitch font --- CLAUDE.md | 2 +- architecture_explore.md | 6 +-- docs/updates/2026-09-b.md | 10 ++++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_command_widget.py | 3 +- .../show_code_window/code_window.py | 3 +- pybreeze/pybreeze_ui/terminal_view.py | 15 +++++- test/test_utils/test_terminal_font.py | 54 +++++++++++++++++++ 8 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 test/test_utils/test_terminal_font.py diff --git a/CLAUDE.md b/CLAUDE.md index 0dee2a59..1cf8a42f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ pybreeze/ │ ├── gui_thread_gc.py # Garbage collected on a GUI-thread timer, never on a worker │ ├── plain_text.py # as_text: server/file text shown in message boxes as text, not markup │ ├── exact_text.py # exact_text: a text box read as typed (toPlainText changes U+00A0, U+2028) -│ ├── terminal_view.py # insert_rewinding: terminal output into a text view, a lone \r rewinding the line +│ ├── terminal_view.py # Terminal output in a text view: fixed-pitch font, a lone \r rewinding the line │ ├── error_text.py # error_text: a tool's English error (exception_tags) in the IDE language │ ├── closing.py # may_close / AskingDock: tabs and docks with unsaved work are asked first │ ├── dialog/ # prthinker settings dialog diff --git a/architecture_explore.md b/architecture_explore.md index e2b154b2..e73b93b2 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -160,7 +160,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `ReaderGrace` / `any_alive()` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 - 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …`、`Task exit with code …`、行程仍握著輸出的註明,共 15 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 - 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 -- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 +- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,與 SSH terminal 共用);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 --- @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 128 個 `test_*.py`、2309 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 129 個 `test_*.py`、2312 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index c143101c..e7b74672 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -808,3 +808,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2309 tests in 128 files. The SSH tests (119) pass. Full suite at 531ea9d: 2292 passed, 15 skipped; both startup tests exit 0. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-290 · 2026-09-24 · Terminal output in a fixed-pitch font · #fix #ssh #ui + +- **What**: the SSH terminal and the run window showed output in the interface's proportional font. Output laid out in columns (`ls -l`, `df`, `ps`, a table a script prints) came out ragged: on Windows an `i` was about 3 px wide and a `W` 12 px. +- **Fix**: `terminal_view.use_terminal_font(view)` gives a view the system's fixed-pitch font (`QFontDatabase.SystemFont.FixedFont`, Courier New on Windows) at the size the view had. Both views call it. +- **Tests**: new `test_terminal_font.py` (3): the helper keeps the size; the SSH terminal and the run window use the fixed font. The two view tests failed on the old code. +- **Result / numbers**: 2312 tests in 129 files. `ruff check` clean. +- **Docs**: `CLAUDE.md` tree line for `terminal_view.py`, `architecture_explore.md` (both views, test count). +- **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/show_code_window/code_window.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_terminal_font.py`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a1284a58..a6f6d2c0 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-290 | 2026-09-24 | Terminal output in a fixed-pitch font | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-289 | 2026-09-24 | A progress bar in the SSH terminal redraws its line | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-288 | 2026-09-24 | Refactor: the run window's line rewinding in a shared module | #refactor | [2026-09-b](2026-09-b.md) | | U-20260924-287 | 2026-09-24 | Command history on the SSH command line | #feature #ssh | [2026-09-b](2026-09-b.md) | @@ -371,4 +372,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 87 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 88 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index ac564da2..0490a492 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -22,7 +22,7 @@ ) from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import load_private_key, unloadable_key_reason from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_login_widget import LoginWidget -from pybreeze.pybreeze_ui.terminal_view import insert_rewinding +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, use_terminal_font from pybreeze.pybreeze_ui.thread_keeper import if_alive, let_run_out from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.utils.logging.logger import pybreeze_logger @@ -248,6 +248,7 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: def _setup_ui(self): self.terminal.setReadOnly(True) self.terminal.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) + use_terminal_font(self.terminal) self.command_input_edit.setPlaceholderText( self.word_dict.get("ssh_command_widget_input_placeholder_command_line") ) diff --git a/pybreeze/pybreeze_ui/show_code_window/code_window.py b/pybreeze/pybreeze_ui/show_code_window/code_window.py index 3f854b85..721af055 100644 --- a/pybreeze/pybreeze_ui/show_code_window/code_window.py +++ b/pybreeze/pybreeze_ui/show_code_window/code_window.py @@ -8,7 +8,7 @@ from PySide6.QtGui import QGuiApplication, QTextCharFormat, QTextCursor from PySide6.QtWidgets import QWidget, QGridLayout, QHBoxLayout, QPlainTextEdit, QPushButton, QScrollArea -from pybreeze.pybreeze_ui.terminal_view import insert_rewinding +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, use_terminal_font from pybreeze.utils.terminal_text import strip_terminal_controls, take_leading_backspaces if TYPE_CHECKING: @@ -58,6 +58,7 @@ def __init__(self): self.code_result = QPlainTextEdit() self.code_result.setLineWrapMode(self.code_result.LineWrapMode.NoWrap) self.code_result.setReadOnly(True) + use_terminal_font(self.code_result) self.code_result.document().setMaximumBlockCount(MAX_OUTPUT_BLOCKS) self.code_result_scroll_area = QScrollArea() self.code_result_scroll_area.setWidgetResizable(True) diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py index bd57c475..cf44efc0 100644 --- a/pybreeze/pybreeze_ui/terminal_view.py +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -4,7 +4,20 @@ """ from __future__ import annotations -from PySide6.QtGui import QTextCharFormat, QTextCursor +from PySide6.QtGui import QFontDatabase, QTextCharFormat, QTextCursor +from PySide6.QtWidgets import QPlainTextEdit + + +def use_terminal_font(view: QPlainTextEdit) -> None: + """Show *view* in the system's fixed-pitch font, at the size it had. + + Output laid out in columns (``ls -l``, ``df``, a table a script prints) + lines up only when every character is as wide as the next; in the + interface's proportional font it came out ragged. + """ + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + font.setPointSizeF(view.font().pointSizeF()) + view.setFont(font) def insert_rewinding(cursor: QTextCursor, text: str, text_format: QTextCharFormat) -> bool: diff --git a/test/test_utils/test_terminal_font.py b/test/test_utils/test_terminal_font.py new file mode 100644 index 00000000..4f3d2228 --- /dev/null +++ b/test/test_utils/test_terminal_font.py @@ -0,0 +1,54 @@ +"""Terminal output is shown in a fixed-pitch font, so columns line up.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtGui import QFontDatabase +from PySide6.QtWidgets import QApplication, QPlainTextEdit + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget +from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow +from pybreeze.pybreeze_ui.terminal_view import use_terminal_font + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +def _fixed_family() -> str: + return QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont).family() + + +def test_the_view_gets_the_fixed_pitch_font_at_its_own_size(app): + view = QPlainTextEdit() + font = view.font() + font.setPointSizeF(13.5) + view.setFont(font) + + use_terminal_font(view) + + assert view.font().family() == _fixed_family() + assert view.font().pointSizeF() == 13.5 + + +def test_the_ssh_terminal_shows_a_fixed_pitch_font(app): + widget = SSHCommandWidget() + try: + assert widget.terminal.font().family() == _fixed_family() + finally: + widget.close() + + +def test_the_run_window_shows_a_fixed_pitch_font(app): + window = CodeWindow() + try: + assert window.code_result.font().family() == _fixed_family() + finally: + window.close() From f0858f7ff9b7b2b834a54e1ee0c2a93719b63ca2 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:35:44 +0800 Subject: [PATCH 021/166] Give the SSH shell the terminal's size as it is resized --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 +- docs/updates/2026-09-b.md | 10 ++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_command_widget.py | 41 +++++++-- pybreeze/pybreeze_ui/terminal_view.py | 21 ++++- test/test_utils/test_ssh_terminal_output.py | 91 ++++++++++++++++++- test/test_utils/test_terminal_font.py | 32 ++++++- 10 files changed, 191 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d0d5e841..f8b5bd14 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 64016db3..af79dc7a 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index d27a9457..bedda7ea 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture_explore.md b/architecture_explore.md index e73b93b2..d0c7efe4 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -456,7 +456,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 129 個 `test_*.py`、2312 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 129 個 `test_*.py`、2319 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index e7b74672..a1ed8f7b 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -818,3 +818,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `CLAUDE.md` tree line for `terminal_view.py`, `architecture_explore.md` (both views, test count). - **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/show_code_window/code_window.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_terminal_font.py`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-291 · 2026-09-24 · The SSH shell's pty follows the terminal's size · #feature #ssh + +- **What**: the SSH shell's pty was opened at 120 x 32 and never changed. Programs laid their output out for that size whatever the view showed: `ls` spread its columns over 120 characters in a view showing 70 (a horizontal scroll to read them), a progress bar sized itself to 120. +- **Change**: `terminal_view.terminal_size(view)` gives the columns and rows a view shows whole, in its (fixed-pitch, U-20260924-290) font, at least 20 x 5. The shell opens at the view's size (`open_shell_channel(client, size)`); a resize of the view's viewport sends `resize_pty` when the size in characters changes (`_follow_view_size`, through the widget's `eventFilter`), and once more when the shell comes up, for a resize while it was connecting. A resize the server refuses (a dropped link) is logged and tried again on the next one. +- **Tests**: `test_ssh_terminal_output.py` +5 (the pty follows the view; a resize within a column sends nothing; nothing without a session; a refused resize is not raised; the shell opens at the given size), `test_terminal_font.py` +2 (`terminal_size` grows with the view; a squeezed view gets the minimum). The follow test failed on the old code. `TestTheConnectMessage` now gives `_start_shell` a channel fake instead of a bare `object()`. +- **Result / numbers**: 2319 tests in 129 files. The terminal, run-window and SSH tests (167) pass. `ruff check` clean. +- **Docs**: the three READMEs' SSH paragraph, `architecture_explore.md` (SSH row, test count). +- **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `test/test_utils/test_terminal_font.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a6f6d2c0..7055fa87 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-291 | 2026-09-24 | The SSH shell's pty follows the terminal's size | #feature #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-290 | 2026-09-24 | Terminal output in a fixed-pitch font | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-289 | 2026-09-24 | A progress bar in the SSH terminal redraws its line | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-288 | 2026-09-24 | Refactor: the run window's line rewinding in a shared module | #refactor | [2026-09-b](2026-09-b.md) | @@ -372,4 +373,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 88 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 89 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 0490a492..712d1bb7 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -22,7 +22,7 @@ ) from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import load_private_key, unloadable_key_reason from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_login_widget import LoginWidget -from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, use_terminal_font +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, terminal_size, use_terminal_font from pybreeze.pybreeze_ui.thread_keeper import if_alive, let_run_out from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.utils.logging.logger import pybreeze_logger @@ -136,15 +136,17 @@ def send_all(channel: paramiko.Channel, data: bytes) -> None: channel.settimeout(0.0) -def open_shell_channel(client: paramiko.SSHClient) -> paramiko.Channel: +def open_shell_channel(client: paramiko.SSHClient, size: tuple[int, int]) -> paramiko.Channel: """Open an interactive shell on *client*'s connection. Waits on the network: not the UI thread. - The channel comes back non-blocking: its reader polls it. + Its pty is *size* (columns, rows). The channel comes back non-blocking: + its reader polls it. """ transport = client.get_transport() if transport is not None: transport.set_keepalive(SSH_KEEPALIVE_SECONDS) - channel = client.invoke_shell(term="xterm", width=120, height=32) + columns, rows = size + channel = client.invoke_shell(term="xterm", width=columns, height=rows) channel.settimeout(0.0) return channel @@ -221,6 +223,8 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: self._connecting: SshConnectThread | None = None # Lines sent, for Up and Down / 送出過的指令 self._history = CommandHistory() + # The size the shell's pty was last given / pty 目前的大小 + self._pty_size: tuple[int, int] | None = None host_key_asker() # built here, on the UI thread, for a connect to ask through if self.add_login_widget: @@ -278,10 +282,14 @@ def _bind_events(self): self.command_input_edit.returnPressed.connect(self.send_command) self.interrupt_button.clicked.connect(self.send_interrupt) self.command_input_edit.installEventFilter(self) + self._terminal_viewport = self.terminal.viewport() + self._terminal_viewport.installEventFilter(self) def eventFilter(self, watched, event) -> bool: - """Keys the command line gives to the shell rather than to its own text.""" - if (watched is self.command_input_edit and event.type() == QEvent.Type.KeyPress + """Keys the command line gives to the shell rather than to its own text; the view's size.""" + if watched is self._terminal_viewport and event.type() == QEvent.Type.Resize: + self._follow_view_size() + elif (watched is self.command_input_edit and event.type() == QEvent.Type.KeyPress and self._command_line_key(event.key(), event.modifiers())): return True return super().eventFilter(watched, event) @@ -308,6 +316,23 @@ def _command_line_key(self, key: int, modifiers) -> bool: self.command_input_edit.setText(line) return True + def _follow_view_size(self) -> None: + """Give the shell's pty the size the view shows, as a terminal window does on a resize. + + It had 120 columns whatever the view's width, so ``ls`` laid its + columns out for a width the view did not have. + """ + size = terminal_size(self.terminal) + if not self._has_shell() or size == self._pty_size: + return + columns, rows = size + try: + self.shell_channel.resize_pty(width=columns, height=rows) + except (OSError, paramiko.SSHException) as error: + pybreeze_logger.debug("SSH pty resize: %r", error) + return + self._pty_size = size + def append_text(self, text: str): """Add a notice of our own, starting on a line of its own.""" end = QTextCursor(self.terminal.document()) @@ -366,6 +391,7 @@ def connect_ssh(self): # connected would otherwise leak the old SSH client and orphan its # reader thread (which keeps appending to the terminal). self._cleanup() + size = self._pty_size = terminal_size(self.terminal) client = paramiko.SSHClient() self.ssh_client = client apply_host_key_policy(client, self) @@ -379,7 +405,7 @@ def connect() -> None: client.connect( hostname=host, port=port, username=user, password=password, timeout=10, disabled_algorithms=SHA1_ALGORITHMS) - opened["channel"] = open_shell_channel(client) + opened["channel"] = open_shell_channel(client, size) # The connect and the shell's channel are made off the UI thread: an # unreachable host used to hold the IDE for the connect, banner and auth # timeouts together, and a server gone quiet after auth held it while @@ -435,6 +461,7 @@ def _start_shell(self, channel: paramiko.Channel, host: str, port: int, user: st self.reader_thread.data_received.connect(self._on_data) self.reader_thread.closed.connect(self._on_closed) self.reader_thread.start() + self._follow_view_size() # resized while it was connecting self.login_widget.status_label.setText( self.word_dict.get("ssh_command_widget_status_label_connected")) # An IPv6 address in brackets, or its port reads as one more group diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py index cf44efc0..07201346 100644 --- a/pybreeze/pybreeze_ui/terminal_view.py +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -4,7 +4,7 @@ """ from __future__ import annotations -from PySide6.QtGui import QFontDatabase, QTextCharFormat, QTextCursor +from PySide6.QtGui import QFontDatabase, QFontMetricsF, QTextCharFormat, QTextCursor from PySide6.QtWidgets import QPlainTextEdit @@ -20,6 +20,25 @@ def use_terminal_font(view: QPlainTextEdit) -> None: view.setFont(font) +# Smallest size given to a program for a view squeezed to almost nothing +MIN_COLUMNS = 20 +MIN_ROWS = 5 + + +def terminal_size(view: QPlainTextEdit) -> tuple[int, int]: + """The columns and rows of text *view* shows whole, in its font. + + What a pty is told, for programs to lay out their output (``ls``'s + columns, a progress bar's width) to fit the view. + """ + metrics = QFontMetricsF(view.font()) + margins = 2 * view.document().documentMargin() + viewport = view.viewport() + columns = int((viewport.width() - margins) // metrics.horizontalAdvance("M")) + rows = int((viewport.height() - margins) // metrics.lineSpacing()) + return max(columns, MIN_COLUMNS), max(rows, MIN_ROWS) + + def insert_rewinding(cursor: QTextCursor, text: str, text_format: QTextCharFormat) -> bool: """Insert *text* at *cursor*, a lone ``\\r`` going back to the start of the line. diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index f7df8d54..95b7f007 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -19,6 +19,7 @@ SSHReaderThread, TerminalDecoder, ) +from pybreeze.pybreeze_ui.terminal_view import terminal_size @pytest.fixture(scope="module") @@ -335,7 +336,7 @@ def test_names_the_session_in_the_ide_language(self, app, monkeypatch, host, sho widget = SSHCommandWidget() widget.word_dict = word - widget._start_shell(object(), host, 22, "alice") + widget._start_shell(ResizableChannel(), host, 22, "alice") assert widget.terminal.toPlainText().endswith(f"已以 alice 身分連線至 {shown}\n") assert widget.login_widget.status_label.text() == "已連線" @@ -390,3 +391,91 @@ def test_sending_goes_back_to_a_new_line(self): assert history.older("") == "a" assert history.older("") == "b" + + +class ResizableChannel: + """Records the pty sizes it is given.""" + + closed = False + + def __init__(self, error: Exception | None = None) -> None: + self.sizes: list[tuple[int, int]] = [] + self.error = error + + def resize_pty(self, width: int, height: int) -> None: + if self.error is not None: + raise self.error + self.sizes.append((width, height)) + + +class TestThePtySize: + @staticmethod + def _shown(app, channel) -> SSHCommandWidget: + widget = SSHCommandWidget() + widget.resize(900, 600) + widget.show() + QApplication.processEvents() + widget.shell_channel = channel + return widget + + @staticmethod + def _resize(widget: SSHCommandWidget, width: int) -> None: + widget.resize(width, 600) + QApplication.processEvents() + + def test_the_pty_follows_the_view(self, app): + # It stayed at 120 columns whatever the view's width + channel = ResizableChannel() + widget = self._shown(app, channel) + + self._resize(widget, 500) + + assert channel.sizes[-1] == terminal_size(widget.terminal) + assert channel.sizes[-1][0] < 120 - 40 + widget.shell_channel = None + widget.close() + + def test_a_resize_within_a_column_sends_nothing(self, app): + channel = ResizableChannel() + widget = self._shown(app, channel) + self._resize(widget, 500) + sent = len(channel.sizes) + + self._resize(widget, 501) + + assert len(channel.sizes) == sent + widget.shell_channel = None + widget.close() + + def test_without_a_session_nothing_is_sent(self, app): + widget = self._shown(app, None) + + self._resize(widget, 500) # no channel to give it to: nothing raised + + widget.close() + + def test_a_resize_the_server_refuses_is_not_raised(self, app): + widget = self._shown(app, ResizableChannel(OSError("link down"))) + + self._resize(widget, 500) + + assert widget._pty_size != terminal_size(widget.terminal) # tried again next time + widget.shell_channel = None + widget.close() + + def test_the_shell_opens_at_the_view_size(self, app): + options = {} + + class Client: + @staticmethod + def get_transport(): + return None + + @staticmethod + def invoke_shell(**given): + options.update(given) + return paramiko.Channel(0) + + ssh_command_widget.open_shell_channel(Client(), (77, 21)) + + assert (options["width"], options["height"]) == (77, 21) diff --git a/test/test_utils/test_terminal_font.py b/test/test_utils/test_terminal_font.py index 4f3d2228..a9bb1043 100644 --- a/test/test_utils/test_terminal_font.py +++ b/test/test_utils/test_terminal_font.py @@ -1,4 +1,4 @@ -"""Terminal output is shown in a fixed-pitch font, so columns line up.""" +"""Terminal views: a fixed-pitch font, so columns line up, and the size a pty is given.""" from __future__ import annotations import os @@ -12,7 +12,7 @@ from pybreeze.extend_multi_language.update_language_dict import update_language_dict from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow -from pybreeze.pybreeze_ui.terminal_view import use_terminal_font +from pybreeze.pybreeze_ui.terminal_view import MIN_COLUMNS, MIN_ROWS, terminal_size, use_terminal_font @pytest.fixture(scope="module") @@ -52,3 +52,31 @@ def test_the_run_window_shows_a_fixed_pitch_font(app): assert window.code_result.font().family() == _fixed_family() finally: window.close() + + +def _view(width: int, height: int) -> QPlainTextEdit: + view = QPlainTextEdit() + use_terminal_font(view) + view.resize(width, height) + view.show() + QApplication.processEvents() + return view + + +def test_a_wider_view_has_more_columns_and_a_taller_one_more_rows(app): + small, wide, tall = _view(400, 300), _view(800, 300), _view(400, 600) + try: + assert terminal_size(wide)[0] > terminal_size(small)[0] + assert terminal_size(tall)[1] > terminal_size(small)[1] + assert terminal_size(wide)[1] == terminal_size(small)[1] + finally: + for view in (small, wide, tall): + view.close() + + +def test_a_squeezed_view_still_gives_a_usable_size(app): + view = _view(10, 10) + try: + assert terminal_size(view) == (MIN_COLUMNS, MIN_ROWS) + finally: + view.close() From 058950fd69adf961df6b26f847f61489445a154d Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:47:38 +0800 Subject: [PATCH 022/166] Show ANSI colours in the SSH terminal --- CLAUDE.md | 1 + README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture.md | 2 +- architecture_explore.md | 5 +- docs/updates/2026-09-b.md | 10 ++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_command_widget.py | 45 +++-- pybreeze/pybreeze_ui/terminal_view.py | 31 +++- pybreeze/utils/terminal_style.py | 154 ++++++++++++++++++ test/test_utils/test_ssh_terminal_output.py | 76 ++++++++- test/test_utils/test_terminal_style.py | 112 +++++++++++++ 13 files changed, 418 insertions(+), 27 deletions(-) create mode 100644 pybreeze/utils/terminal_style.py create mode 100644 test/test_utils/test_terminal_style.py diff --git a/CLAUDE.md b/CLAUDE.md index 1cf8a42f..9b7b8617 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,6 +52,7 @@ pybreeze/ ├── exception/ # ITEException hierarchy ├── logging/ file_process/ app_dirs.py / subprocess_util.py ├── terminal_text.py # Escape sequences and controls stripped from terminal output (SSH, run window) + ├── terminal_style.py # SGR colours and emphasis read into a TextStyle (SSH terminal) └── manager/package_manager/ # PackageManager — holds syntax_check_list ``` diff --git a/README.md b/README.md index f8b5bd14..0b2c430c 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with ANSI handling and keepalive, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index af79dc7a..afa5522d 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 ANSI 处理与 keepalive 的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index bedda7ea..2243903a 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 ANSI 處理與 keepalive 的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture.md b/architecture.md index 007211b4..15abc2c2 100644 --- a/architecture.md +++ b/architecture.md @@ -37,7 +37,7 @@ their output reaches the UI through Queue + QTimer. | `pybreeze/extend/process_executor/` | Subprocess isolation layer: `TaskProcessManager`, `process_executor_utils.py`, `FileRunnerProcess`, `queue_pump.py`, one sub-package per automation package, plus `test_pioneer/` and `prthinker/` | | `pybreeze/extend/mail_thunder_extend/`, `prthinker_extend/` | Post-test email hook; prthinker settings and argument assembly (pure logic) | | `pybreeze/extend_multi_language/` | PyBreeze's English and Traditional Chinese strings, merged into JEditor's dictionaries | -| `pybreeze/utils/` | Pure logic, no Qt or JEditor (`test_utils_has_no_qt.py` guards it): request parsing and codegen, HTTP tools, `network/` SSRF validation, pinned connections and capped reads, exceptions, logging, `app_dirs.py`, `subprocess_util.py`, `terminal_text.py` (terminal escapes stripped for the SSH terminal and the run window) | +| `pybreeze/utils/` | Pure logic, no Qt or JEditor (`test_utils_has_no_qt.py` guards it): request parsing and codegen, HTTP tools, `network/` SSRF validation, pinned connections and capped reads, exceptions, logging, `app_dirs.py`, `subprocess_util.py`, `terminal_text.py` (terminal escapes stripped for the SSH terminal and the run window), `terminal_style.py` (SGR colours read for the SSH terminal) | | `test/test_utils/` | Unit tests (pure logic and headless widgets). `test/unit_test/start_automation/` holds the launch tests | | `pyproject.toml`, `dev.toml` | Stable packaging (CI bumps and publishes it) and the unpublished dev packaging (keep its dependencies identical) | | `.github/workflows/` | `dev.yml`, `stable.yml` (unit tests on a Windows matrix, then SonarCloud) | diff --git a/architecture_explore.md b/architecture_explore.md index d0c7efe4..97d317fb 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成文字:UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -342,6 +342,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `app_dirs.py` | `pybreeze_data_dir()` → `~/.pybreeze`,所有持久化資料的單一位置,建立時為 `0700`(`DATA_DIR_MODE`);`pybreeze_data_path()` 只給路徑、不建立 | | `terminal_text.py` | 終端輸出的 escape 與控制字元:`strip_terminal_controls()`(CSI、OSC/DCS 等控制字串、nF、兩位元組 escape 剝除,backspace 套用,其餘 C0 丟掉)、`split_incomplete_escape()`(讀取切斷在 escape 中間時把尾巴留給下一次)、`split_unfinished_end()`(再加上它前面或最後的 `\r`)、`take_leading_backspaces()`(一段開頭的 backspace 留給畫面,擦掉前一段已經顯示的字,不越過行首)。SSH terminal 與執行視窗共用 | +| `terminal_style.py` | SGR(`ESC [ … m`)讀成 `TextStyle`(frozen dataclass:前景、背景、粗體、斜體、底線、反白):`apply_sgr()`(16 色與亮色、`38;5;n` 256 色、`38;2;r;g;b` 24 位元色、各開關與 39/49 預設、0 重設;看不懂或格式錯的參數不改任何東西,超過 5 位數的參數略過,`int()` 不收超過 4300 位數)、`split_styled()`(文字在 SGR 處切段,每段帶它的樣式,其餘 escape 留給 `strip_terminal_controls()`)、`colour_rgb()`(xterm 的色盤)。只有 SSH terminal 用:執行視窗的程式寫到 pipe,不會上色 | | `subprocess_util.py` | `utf8_subprocess_env()`(釘 `PYTHONIOENCODING`,解 Windows cp950 亂碼)、`no_window_creationflags()`(`CREATE_NO_WINDOW`,避免 GUI 程式彈出黑窗) | | `logging/logger.py` | `pybreeze_logger`(具名 logger,**不動 root logger**)+ `PyBreezeLogger(RotatingFileHandler)`:寫到 `~/.pybreeze/logs/PyBreeze.log`(`PYBREEZE_LOG_FILE` 可改),UTF-8、附加模式、每行帶行程編號,第一筆紀錄才開檔;只在開檔時輪替,門檻 `PYBREEZE_LOG_MAX_BYTES`(預設 100 MB);開不了檔就改寫 `os.devnull` 並警告一次。與 JEditor、FrontEngine 同一套做法(工作區 X-6) | | `exception/` | `ITEException` 為根的 17 個例外類別 + `exception_tags.py` 訊息常數;`error_templates.py` 把名稱以 `_error` 結尾的常數變成語言字典的 `error_text_<名稱>`(英文字典直接取常數本身) | @@ -456,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 129 個 `test_*.py`、2319 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2360 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index a1ed8f7b..1b22cb26 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -828,3 +828,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: the three READMEs' SSH paragraph, `architecture_explore.md` (SSH row, test count). - **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_terminal_output.py`, `test/test_utils/test_terminal_font.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-292 · 2026-09-24 · Colours in the SSH terminal · #feature #ssh #ui + +- **What**: the SSH terminal removed every escape sequence, colours with the rest. Through a pty, `ls --color`, `git`, `grep --color`, compilers and test runners colour their output, and it came out all one colour. +- **Change**: new `pybreeze/utils/terminal_style.py` (pure): `apply_sgr()` reads an SGR sequence (`ESC [ … m`) into a frozen `TextStyle`: the 16 basic and bright colours, `38;5;n` / `48;5;n` (256 colours), `38;2;r;g;b` (24-bit), bold, italic, underline, inverse and their off switches, 39/49 defaults, 0 or nothing to reset. Unknown or malformed parameters change nothing; a parameter longer than 5 digits is ignored (`int()` refuses past 4300 digits, which a server could send). `split_styled()` cuts text at its SGR sequences, each piece with its style; `colour_rgb()` gives xterm's colours. `terminal_view.style_format()` turns a style into a `QTextCharFormat` (the plain style is an empty format; inverse takes the view's own colours for a default). `TerminalDecoder.feed()` now returns the pieces, the style carried from one read to the next and dropped on `reset()`. `SSHCommandWidget` keeps a lone `\r` that ends a piece for the next one (`_rewind_pending`, as the run window does), so a bar that changes colour after its rewind still redraws its line. The run window still removes colours: a program writing to a pipe does not colour its output. +- **Tests**: new `test_terminal_style.py` (35: parameters, resets, malformed and huge parameters, the 256 palette, pieces, formats), `test_ssh_terminal_output.py` +6 (colours shown, carried across reads, reset for a new session, a bar recoloured after its rewind in one read and in two, a line ending around a reset). The decoder tests read the pieces' text. An ad-hoc Hypothesis run (7,500 examples: random SGR parameters, text with SGR, rewinds, backspaces and cut escapes, through the parser and the widget) found nothing. +- **Result / numbers**: 2360 tests in 130 files. Full suite at f0858f7: 2304 passed, 15 skipped; both startup tests exit 0; gates clean. `ruff check` clean. +- **Docs**: the three READMEs' SSH paragraph, `CLAUDE.md` tree, `architecture.md` (utils row), `architecture_explore.md` (SSH row, utils table, test count). +- **Files**: `pybreeze/utils/terminal_style.py`, `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_terminal_style.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `CLAUDE.md`, `architecture.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7055fa87..36b7f24b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-292 | 2026-09-24 | Colours in the SSH terminal | #feature #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-291 | 2026-09-24 | The SSH shell's pty follows the terminal's size | #feature #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-290 | 2026-09-24 | Terminal output in a fixed-pitch font | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-289 | 2026-09-24 | A progress bar in the SSH terminal redraws its line | #fix #ssh | [2026-09-b](2026-09-b.md) | @@ -373,4 +374,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 89 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 90 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 712d1bb7..93c36a44 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -22,10 +22,11 @@ ) from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import load_private_key, unloadable_key_reason from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_login_widget import LoginWidget -from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, terminal_size, use_terminal_font +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, style_format, terminal_size, use_terminal_font from pybreeze.pybreeze_ui.thread_keeper import if_alive, let_run_out from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.utils.logging.logger import pybreeze_logger +from pybreeze.utils.terminal_style import PLAIN, TextStyle, split_styled from pybreeze.utils.terminal_text import split_unfinished_end, strip_terminal_controls, take_leading_backspaces # What closing a channel or a client can raise on a connection already broken @@ -38,27 +39,37 @@ class TerminalDecoder: A read ends wherever the channel's buffer did, so it can stop inside a multi-byte UTF-8 character or inside an escape sequence. Decoding each read on its own showed the character as replacement marks and the escape's tail - as text; this carries the unfinished part over to the next read. + as text; this carries the unfinished part over to the next read. The + colours and emphasis SGR sequences set carry over too. """ def __init__(self) -> None: self._decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") self._pending = "" + self._style = PLAIN def reset(self) -> None: """Forget anything carried over, for a new session.""" self._decoder.reset() self._pending = "" + self._style = PLAIN - def feed(self, data: bytes) -> str: - """Return the text *data* completes, escape sequences removed. + def feed(self, data: bytes) -> list[tuple[TextStyle, str]]: + """Return the text *data* completes, in pieces with the style each is shown in. - Backspaces it starts with are kept, for the view to take back what an - earlier read showed; any others are applied here. + Escape sequences are removed. Backspaces a piece starts with are kept, + for the view to take back what it already showed; any others are + applied here. """ text, self._pending = split_unfinished_end(self._pending + self._decoder.decode(data)) - backspaces, text = take_leading_backspaces(text) - return "\x08" * backspaces + strip_terminal_controls(text) + pieces, self._style = split_styled(text, self._style) + return [(style, _shown(piece)) for style, piece in pieces] + + +def _shown(text: str) -> str: + """*text* as the view shows it, the backspaces it starts with kept.""" + backspaces, text = take_leading_backspaces(text) + return "\x08" * backspaces + strip_terminal_controls(text) # Bound the terminal scrollback so an endless stream (``tail -f``, ``yes``) @@ -225,6 +236,8 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: self._history = CommandHistory() # The size the shell's pty was last given / pty 目前的大小 self._pty_size: tuple[int, int] | None = None + # The output so far ended on a lone \r the next piece applies / 待套用的 \r + self._rewind_pending = False host_key_asker() # built here, on the UI thread, for a connect to ask through if self.add_login_widget: @@ -339,8 +352,8 @@ def append_text(self, text: str): end.movePosition(QTextCursor.MoveOperation.End) self._insert_output(text if end.atBlockStart() else "\n" + text) - def _insert_output(self, text: str) -> None: - """Add *text* where the output ends, without starting a new line. + def _insert_output(self, text: str, text_format: QTextCharFormat | None = None) -> None: + """Add *text* where the output ends, without starting a new line, in *text_format*. ``appendPlainText`` starts a new paragraph on every call, so each read from the shell began on a line of its own, wherever the read happened @@ -351,14 +364,17 @@ def _insert_output(self, text: str) -> None: end = QTextCursor(self.terminal.document()) end.movePosition(QTextCursor.MoveOperation.End) backspaces, text = take_leading_backspaces(text) - if backspaces: + if self._rewind_pending: + # A lone \r before a colour change ("50%\r" "\x1b[32m60%") applies here + text = "\r" + text + elif backspaces: # They take back what an earlier read showed, never past the line's start end.movePosition(QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, min(backspaces, end.positionInBlock())) end.removeSelectedText() # A lone \r redraws the line (a progress bar); the decoder holds one # that ends a read until the next shows whether "\n" follows - insert_rewinding(end, text, QTextCharFormat()) + self._rewind_pending = insert_rewinding(end, text, text_format or QTextCharFormat()) if following: scroll_bar.setValue(scroll_bar.maximum()) @@ -457,6 +473,7 @@ def _start_shell(self, channel: paramiko.Channel, host: str, port: int, user: st """Show *channel*'s output from now on. UI thread; nothing here waits on the network.""" self.shell_channel = channel self._decoder.reset() + self._rewind_pending = False self.reader_thread = SSHReaderThread(self.shell_channel) self.reader_thread.data_received.connect(self._on_data) self.reader_thread.closed.connect(self._on_closed) @@ -470,7 +487,9 @@ def _start_shell(self, channel: paramiko.Channel, host: str, port: int, user: st host=shown_host, port=port, user=user) + "\n") def _on_data(self, data: bytes): - self._insert_output(self._decoder.feed(data)) + palette = self.terminal.palette() + for style, text in self._decoder.feed(data): + self._insert_output(text, style_format(style, palette)) def _on_closed(self, msg: str): """The shell ended on the server's side (``exit``, a dropped link). diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py index 07201346..efa4965f 100644 --- a/pybreeze/pybreeze_ui/terminal_view.py +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -4,9 +4,11 @@ """ from __future__ import annotations -from PySide6.QtGui import QFontDatabase, QFontMetricsF, QTextCharFormat, QTextCursor +from PySide6.QtGui import QColor, QFont, QFontDatabase, QFontMetricsF, QPalette, QTextCharFormat, QTextCursor from PySide6.QtWidgets import QPlainTextEdit +from pybreeze.utils.terminal_style import PLAIN, Colour, TextStyle, colour_rgb + def use_terminal_font(view: QPlainTextEdit) -> None: """Show *view* in the system's fixed-pitch font, at the size it had. @@ -39,6 +41,33 @@ def terminal_size(view: QPlainTextEdit) -> tuple[int, int]: return max(columns, MIN_COLUMNS), max(rows, MIN_ROWS) +def _qcolour(colour: Colour | None) -> QColor | None: + return None if colour is None else QColor(*colour_rgb(colour)) + + +def style_format(style: TextStyle, palette: QPalette) -> QTextCharFormat: + """The format for text in *style*; *palette* gives the view's own colours, for inverse. + + The plain style is an empty format: the view's own font and colours. + """ + text_format = QTextCharFormat() + if style == PLAIN: + return text_format + foreground, background = _qcolour(style.foreground), _qcolour(style.background) + if style.inverse: + foreground, background = (background or palette.color(QPalette.ColorRole.Base), + foreground or palette.color(QPalette.ColorRole.Text)) + if foreground is not None: + text_format.setForeground(foreground) + if background is not None: + text_format.setBackground(background) + if style.bold: + text_format.setFontWeight(QFont.Weight.Bold) + text_format.setFontItalic(style.italic) + text_format.setFontUnderline(style.underline) + return text_format + + def insert_rewinding(cursor: QTextCursor, text: str, text_format: QTextCharFormat) -> bool: """Insert *text* at *cursor*, a lone ``\\r`` going back to the start of the line. diff --git a/pybreeze/utils/terminal_style.py b/pybreeze/utils/terminal_style.py new file mode 100644 index 00000000..770638e5 --- /dev/null +++ b/pybreeze/utils/terminal_style.py @@ -0,0 +1,154 @@ +"""The colours and emphasis a program's SGR escape sequences ask for. + +``ESC [ … m`` (Select Graphic Rendition) sets how the text after it looks: +``ls --color``, ``git``, ``grep --color`` use it through a pty. This reads the +sequences into a :class:`TextStyle`, carried from one read to the next; the +view turns it into a text format. Other escape sequences are not read here. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass, replace + +# A colour: an index into the 256-colour palette, or (red, green, blue) +Colour = int | tuple[int, int, int] + +# SGR with ";"-separated parameters. The ":" form (ESC [ 38:5:196 m) is left +# to strip_terminal_controls, which removes it. +SGR_PATTERN = re.compile(r'\x1B\[([0-9;]*)m') + +# Parameter -> (attribute, value) for the on/off parameters +_SWITCHES = { + 1: ("bold", True), 22: ("bold", False), + 3: ("italic", True), 23: ("italic", False), + 4: ("underline", True), 24: ("underline", False), + 7: ("inverse", True), 27: ("inverse", False), +} +_DEFAULT_FOREGROUND = 39 +_DEFAULT_BACKGROUND = 49 +_EXTENDED_FOREGROUND = 38 +_EXTENDED_BACKGROUND = 48 +# Parameter runs that pick one of the 16 basic colours: first, attribute, first colour +_COLOUR_RUNS = ((30, "foreground", 0), (90, "foreground", 8), (40, "background", 0), (100, "background", 8)) +_RUN_LENGTH = 8 +_PALETTE_FORM = 5 # 38;5;n +_RGB_FORM = 2 # 38;2;r;g;b + +# xterm's first 16 colours: normal, then bright +_BASIC_COLOURS = ( + (0, 0, 0), (205, 0, 0), (0, 205, 0), (205, 205, 0), + (0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229), + (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), + (92, 92, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255), +) +_CUBE_LEVELS = (0, 95, 135, 175, 215, 255) +_CUBE_START = 16 +_GREY_START = 232 +_PALETTE_SIZE = 256 +_CHANNEL_MAX = 255 +# Longest parameter read: past it (a server can send thousands of digits, and +# int() refuses more than 4300) it is one no parameter means +_MAX_PARAMETER_DIGITS = 5 +_UNKNOWN = -1 + + +@dataclass(frozen=True) +class TextStyle: + """How text looks; ``None`` colours are the view's own.""" + + foreground: Colour | None = None + background: Colour | None = None + bold: bool = False + italic: bool = False + underline: bool = False + inverse: bool = False + + +PLAIN = TextStyle() + + +def colour_rgb(colour: Colour) -> tuple[int, int, int]: + """The (red, green, blue) of *colour*, as xterm shows it.""" + if isinstance(colour, tuple): + return colour + if colour < len(_BASIC_COLOURS): + return _BASIC_COLOURS[colour] + if colour < _GREY_START: + index = colour - _CUBE_START + return (_CUBE_LEVELS[index // 36], _CUBE_LEVELS[index // 6 % 6], _CUBE_LEVELS[index % 6]) + grey = 8 + 10 * (colour - _GREY_START) + return grey, grey, grey + + +def _extended_colour(numbers: list[int], start: int) -> tuple[Colour | None, int]: + """The colour ``38;…``/``48;…`` gives from *start*, and where the next parameter is. + + ``None`` for one malformed or out of range, which leaves the colour as it was. + """ + form = numbers[start] if start < len(numbers) else None + if form == _PALETTE_FORM and start + 1 < len(numbers): + index = numbers[start + 1] + return (index if 0 <= index < _PALETTE_SIZE else None), start + 2 + if form == _RGB_FORM and start + 3 < len(numbers): + rgb = tuple(numbers[start + 1:start + 4]) + return (rgb if 0 <= min(rgb) and max(rgb) <= _CHANNEL_MAX else None), start + 4 + return None, len(numbers) # the rest cannot be read + + +def _colour_parameter(style: TextStyle, number: int) -> TextStyle: + """*style* after one of the basic colour parameters (30–49, 90–107), or as it was.""" + for first, attribute, offset in _COLOUR_RUNS: + if first <= number < first + _RUN_LENGTH: + return replace(style, **{attribute: number - first + offset}) + if number == _DEFAULT_FOREGROUND: + return replace(style, foreground=None) + if number == _DEFAULT_BACKGROUND: + return replace(style, background=None) + return style + + +def _parameter(part: str) -> int: + """One SGR parameter: empty is 0, one too long to be real is ``_UNKNOWN``.""" + if not part: + return 0 + return int(part) if len(part) <= _MAX_PARAMETER_DIGITS else _UNKNOWN + + +def apply_sgr(style: TextStyle, parameters: str) -> TextStyle: + """*style* after an SGR sequence with *parameters* (``"1;31"``); unknown ones are ignored.""" + numbers = [_parameter(part) for part in parameters.split(";")] + index = 0 + while index < len(numbers): + number = numbers[index] + index += 1 + if number == 0: + style = PLAIN + elif number in _SWITCHES: + attribute, value = _SWITCHES[number] + style = replace(style, **{attribute: value}) + elif number in (_EXTENDED_FOREGROUND, _EXTENDED_BACKGROUND): + colour, index = _extended_colour(numbers, index) + if colour is not None: + attribute = "foreground" if number == _EXTENDED_FOREGROUND else "background" + style = replace(style, **{attribute: colour}) + else: + style = _colour_parameter(style, number) + return style + + +def split_styled(text: str, style: TextStyle) -> tuple[list[tuple[TextStyle, str]], TextStyle]: + """*text* cut at its SGR sequences, each piece with the style it is shown in. + + *style* is what earlier text left; the style *text* leaves is returned + with the pieces. The pieces keep every other escape sequence and control. + """ + pieces: list[tuple[TextStyle, str]] = [] + start = 0 + for match in SGR_PATTERN.finditer(text): + if match.start() > start: + pieces.append((style, text[start:match.start()])) + style = apply_sgr(style, match.group(1)) + start = match.end() + if start < len(text): + pieces.append((style, text[start:])) + return pieces, style diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index 95b7f007..f3ee6340 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -8,6 +8,7 @@ import paramiko import pytest from PySide6.QtCore import Qt +from PySide6.QtGui import QColor from PySide6.QtTest import QTest from PySide6.QtWidgets import QApplication @@ -20,6 +21,7 @@ TerminalDecoder, ) from pybreeze.pybreeze_ui.terminal_view import terminal_size +from pybreeze.utils.terminal_style import PLAIN @pytest.fixture(scope="module") @@ -29,20 +31,25 @@ def app(): return instance +def _text(pieces) -> str: + """What the decoder's pieces show, their styles left out.""" + return "".join(text for _style, text in pieces) + + class TestTerminalDecoder: @pytest.mark.parametrize("cut", range(1, 6)) def test_a_character_cut_between_reads_is_joined(self, cut): data = "中文".encode("utf-8") # six bytes, two characters decoder = TerminalDecoder() - assert decoder.feed(data[:cut]) + decoder.feed(data[cut:]) == "中文" + assert _text(decoder.feed(data[:cut])) + _text(decoder.feed(data[cut:])) == "中文" @pytest.mark.parametrize("cut", range(1, 5)) def test_an_escape_cut_between_reads_is_still_removed(self, cut): data = b"\x1b[31mred" decoder = TerminalDecoder() - assert decoder.feed(data[:cut]) + decoder.feed(data[cut:]) == "red" + assert _text(decoder.feed(data[:cut])) + _text(decoder.feed(data[cut:])) == "red" @pytest.mark.parametrize("data", [b"\x1b(Bok", b"\x1b]0;title\x1b\\ok", b"\x1bP1$r0m\x1b\\ok"]) def test_a_character_set_or_string_escape_cut_anywhere_is_removed(self, data): @@ -50,13 +57,13 @@ def test_a_character_set_or_string_escape_cut_anywhere_is_removed(self, data): for cut in range(1, len(data) - 2): decoder = TerminalDecoder() - assert decoder.feed(data[:cut]) + decoder.feed(data[cut:]) == "ok", cut + assert _text(decoder.feed(data[:cut])) + _text(decoder.feed(data[cut:])) == "ok", cut def test_text_before_an_unfinished_escape_is_shown_now(self): decoder = TerminalDecoder() - assert decoder.feed(b"ready \x1b[") == "ready " - assert decoder.feed(b"0mgo") == "go" + assert _text(decoder.feed(b"ready \x1b[")) == "ready " + assert _text(decoder.feed(b"0mgo")) == "go" def test_a_very_long_unterminated_sequence_is_not_held_forever(self): decoder = TerminalDecoder() @@ -73,7 +80,7 @@ def test_reset_forgets_what_was_carried_over(self): decoder.reset() - assert decoder.feed(b"plain") == "plain" + assert _text(decoder.feed(b"plain")) == "plain" class TestTheTerminal: @@ -479,3 +486,60 @@ def invoke_shell(**given): ssh_command_widget.open_shell_channel(Client(), (77, 21)) assert (options["width"], options["height"]) == (77, 21) + + +def _colour_at(widget: SSHCommandWidget, position: int): + cursor = widget.terminal.textCursor() + cursor.setPosition(position + 1) # the format of the character before the cursor + return cursor.charFormat().foreground().color() + + +class TestColours: + def test_output_shows_the_colours_it_asks_for(self, app): + # They were removed: ls --color, git and grep came out all one colour + widget = SSHCommandWidget() + + widget._on_data(b"\x1b[31mred\x1b[0m plain") + + assert widget.terminal.toPlainText() == "red plain" + assert _colour_at(widget, 0) == QColor(205, 0, 0) + assert _colour_at(widget, 4) != QColor(205, 0, 0) + widget.close() + + def test_a_colour_carries_over_to_the_next_read(self, app): + widget = SSHCommandWidget() + + widget._on_data(b"\x1b[32mgr") + widget._on_data(b"een") + + assert _colour_at(widget, 4) == QColor(0, 205, 0) + widget.close() + + def test_a_new_session_starts_without_the_last_one_colour(self, app): + decoder = TerminalDecoder() + decoder.feed(b"\x1b[31m") + + decoder.reset() + + assert decoder.feed(b"plain") == [(PLAIN, "plain")] + + @pytest.mark.parametrize("chunks", [ + [b"50%\r\x1b[32m60%"], + [b"50%\r\x1b[32m", b"60%"], + ]) + def test_a_bar_that_changes_colour_still_redraws_its_line(self, app, chunks): + widget = SSHCommandWidget() + + for chunk in chunks: + widget._on_data(chunk) + + assert widget.terminal.toPlainText() == "60%" + widget.close() + + def test_a_line_ending_around_a_colour_reset_is_one_line_break(self, app): + widget = SSHCommandWidget() + + widget._on_data(b"a\r\x1b[0m\nb") + + assert widget.terminal.toPlainText() == "a\nb" + widget.close() diff --git a/test/test_utils/test_terminal_style.py b/test/test_utils/test_terminal_style.py new file mode 100644 index 00000000..957fbe82 --- /dev/null +++ b/test/test_utils/test_terminal_style.py @@ -0,0 +1,112 @@ +"""SGR escape sequences read into a text style, and the format a view shows it in.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtGui import QColor, QFont, QPalette +from PySide6.QtWidgets import QApplication + +from pybreeze.pybreeze_ui.terminal_view import style_format +from pybreeze.utils.terminal_style import PLAIN, TextStyle, apply_sgr, colour_rgb, split_styled + + +class TestApplySgr: + @pytest.mark.parametrize(("parameters", "expected"), [ + ("31", TextStyle(foreground=1)), + ("91", TextStyle(foreground=9)), + ("44", TextStyle(background=4)), + ("107", TextStyle(background=15)), + ("1;4;3;7", TextStyle(bold=True, underline=True, italic=True, inverse=True)), + ("38;5;196", TextStyle(foreground=196)), + ("48;2;10;20;30", TextStyle(background=(10, 20, 30))), + ("38;5;208;1", TextStyle(foreground=208, bold=True)), + ]) + def test_sets_what_it_names(self, parameters, expected): + assert apply_sgr(PLAIN, parameters) == expected + + @pytest.mark.parametrize("parameters", ["0", "", "00"]) + def test_zero_or_nothing_resets(self, parameters): + styled = TextStyle(foreground=1, background=2, bold=True, underline=True) + + assert apply_sgr(styled, parameters) == PLAIN + + def test_the_off_switches_and_defaults_undo_one_thing_each(self): + styled = TextStyle(foreground=1, background=2, bold=True, italic=True, underline=True, inverse=True) + + assert apply_sgr(styled, "22;23") == TextStyle(foreground=1, background=2, underline=True, inverse=True) + assert apply_sgr(styled, "39;49;24;27") == TextStyle(bold=True, italic=True) + + @pytest.mark.parametrize("parameters", ["38;5", "38;5;256", "38;2;1;2", "38;2;1;2;300", "38;9;1", "38", "5", "1000"]) + def test_what_cannot_be_read_changes_nothing(self, parameters): + styled = TextStyle(foreground=3) + + assert apply_sgr(styled, parameters).foreground == 3 + + def test_a_parameter_thousands_of_digits_long_is_ignored(self): + # int() refuses more than 4300 digits: a server could send them + assert apply_sgr(PLAIN, "9" * 5000 + ";31") == TextStyle(foreground=1) + + +class TestColourRgb: + @pytest.mark.parametrize(("colour", "rgb"), [ + (1, (205, 0, 0)), + (15, (255, 255, 255)), + (16, (0, 0, 0)), + (196, (255, 0, 0)), + (231, (255, 255, 255)), + (232, (8, 8, 8)), + (255, (238, 238, 238)), + ((1, 2, 3), (1, 2, 3)), + ]) + def test_as_xterm_shows_it(self, colour, rgb): + assert colour_rgb(colour) == rgb + + +class TestSplitStyled: + def test_each_piece_has_the_style_it_is_shown_in(self): + pieces, style = split_styled("a\x1b[1;31mred\x1b[0m plain", PLAIN) + + assert pieces == [(PLAIN, "a"), (TextStyle(foreground=1, bold=True), "red"), (PLAIN, " plain")] + assert style == PLAIN + + def test_the_style_carries_on_from_earlier_text(self): + pieces, style = split_styled("still red\x1b[4m", TextStyle(foreground=1)) + + assert pieces == [(TextStyle(foreground=1), "still red")] + assert style == TextStyle(foreground=1, underline=True) + + def test_other_escapes_and_controls_stay_for_the_text_cleaning(self): + pieces, _style = split_styled("\x1b[2Kbar\r\x1b]0;t\x07", PLAIN) + + assert pieces == [(PLAIN, "\x1b[2Kbar\r\x1b]0;t\x07")] + + +@pytest.fixture(scope="module") +def palette(): + QApplication.instance() or QApplication([]) + return QPalette() + + +class TestStyleFormat: + def test_the_plain_style_changes_nothing(self, palette): + text_format = style_format(PLAIN, palette) + + assert not text_format.hasProperty(text_format.Property.ForegroundBrush) + assert not text_format.hasProperty(text_format.Property.FontWeight) + + def test_colours_and_emphasis(self, palette): + text_format = style_format(TextStyle(foreground=1, background=(1, 2, 3), bold=True, underline=True), palette) + + assert text_format.foreground().color() == QColor(205, 0, 0) + assert text_format.background().color() == QColor(1, 2, 3) + assert text_format.fontWeight() == QFont.Weight.Bold + assert text_format.fontUnderline() + + def test_inverse_swaps_the_view_colours_in_for_defaults(self, palette): + text_format = style_format(TextStyle(foreground=1, inverse=True), palette) + + assert text_format.foreground().color() == palette.color(QPalette.ColorRole.Base) + assert text_format.background().color() == QColor(205, 0, 0) From d261735c800445a6a94c0ea306de1f58fd48d2ba Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:52:40 +0800 Subject: [PATCH 023/166] Pick terminal colours that read on the theme's background --- architecture_explore.md | 4 +-- docs/updates/2026-09-b.md | 10 ++++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/terminal_view.py | 18 +++++++--- pybreeze/utils/terminal_style.py | 37 ++++++++++++++------- test/test_utils/test_ssh_terminal_output.py | 10 +++--- test/test_utils/test_terminal_style.py | 37 +++++++++++++++++---- 7 files changed, 88 insertions(+), 31 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 97d317fb..9a7d19f9 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -342,7 +342,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `app_dirs.py` | `pybreeze_data_dir()` → `~/.pybreeze`,所有持久化資料的單一位置,建立時為 `0700`(`DATA_DIR_MODE`);`pybreeze_data_path()` 只給路徑、不建立 | | `terminal_text.py` | 終端輸出的 escape 與控制字元:`strip_terminal_controls()`(CSI、OSC/DCS 等控制字串、nF、兩位元組 escape 剝除,backspace 套用,其餘 C0 丟掉)、`split_incomplete_escape()`(讀取切斷在 escape 中間時把尾巴留給下一次)、`split_unfinished_end()`(再加上它前面或最後的 `\r`)、`take_leading_backspaces()`(一段開頭的 backspace 留給畫面,擦掉前一段已經顯示的字,不越過行首)。SSH terminal 與執行視窗共用 | -| `terminal_style.py` | SGR(`ESC [ … m`)讀成 `TextStyle`(frozen dataclass:前景、背景、粗體、斜體、底線、反白):`apply_sgr()`(16 色與亮色、`38;5;n` 256 色、`38;2;r;g;b` 24 位元色、各開關與 39/49 預設、0 重設;看不懂或格式錯的參數不改任何東西,超過 5 位數的參數略過,`int()` 不收超過 4300 位數)、`split_styled()`(文字在 SGR 處切段,每段帶它的樣式,其餘 escape 留給 `strip_terminal_controls()`)、`colour_rgb()`(xterm 的色盤)。只有 SSH terminal 用:執行視窗的程式寫到 pipe,不會上色 | +| `terminal_style.py` | SGR(`ESC [ … m`)讀成 `TextStyle`(frozen dataclass:前景、背景、粗體、斜體、底線、反白):`apply_sgr()`(16 色與亮色、`38;5;n` 256 色、`38;2;r;g;b` 24 位元色、各開關與 39/49 預設、0 重設;看不懂或格式錯的參數不改任何東西,超過 5 位數的參數略過,`int()` 不收超過 4300 位數)、`split_styled()`(文字在 SGR 處切段,每段帶它的樣式,其餘 escape 留給 `strip_terminal_controls()`)、`colour_rgb(colour, on_dark=)`(前 16 色用 VS Code 終端機的預設值,深色與淺色主題各一組,`terminal_view.style_format()` 依 view 背景的亮度挑;256 色的色塊、灰階與 24 位元色照 xterm)。只有 SSH terminal 用:執行視窗的程式寫到 pipe,不會上色 | | `subprocess_util.py` | `utf8_subprocess_env()`(釘 `PYTHONIOENCODING`,解 Windows cp950 亂碼)、`no_window_creationflags()`(`CREATE_NO_WINDOW`,避免 GUI 程式彈出黑窗) | | `logging/logger.py` | `pybreeze_logger`(具名 logger,**不動 root logger**)+ `PyBreezeLogger(RotatingFileHandler)`:寫到 `~/.pybreeze/logs/PyBreeze.log`(`PYBREEZE_LOG_FILE` 可改),UTF-8、附加模式、每行帶行程編號,第一筆紀錄才開檔;只在開檔時輪替,門檻 `PYBREEZE_LOG_MAX_BYTES`(預設 100 MB);開不了檔就改寫 `os.devnull` 並警告一次。與 JEditor、FrontEngine 同一套做法(工作區 X-6) | | `exception/` | `ITEException` 為根的 17 個例外類別 + `exception_tags.py` 訊息常數;`error_templates.py` 把名稱以 `_error` 結尾的常數變成語言字典的 `error_text_<名稱>`(英文字典直接取常數本身) | @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2360 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2371 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 1b22cb26..1617f2c7 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -838,3 +838,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: the three READMEs' SSH paragraph, `CLAUDE.md` tree, `architecture.md` (utils row), `architecture_explore.md` (SSH row, utils table, test count). - **Files**: `pybreeze/utils/terminal_style.py`, `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_terminal_style.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `CLAUDE.md`, `architecture.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-293 · 2026-09-24 · Terminal colours readable on a dark and on a light theme · #fix #ssh #ui + +- **What**: U-20260924-292 showed the 16 basic colours as xterm does. Rendered on the IDE's dark theme, xterm's blue (0, 0, 238), which `ls --color` uses for directories, was all but unreadable; on a light theme xterm's "white" (229, 229, 229) is. +- **Fix**: `terminal_style.colour_rgb(colour, *, on_dark)` takes the first 16 colours from VS Code's terminal defaults, one set for dark themes and one for light (`terminalColorRegistry.ts`); the 256-colour cube, the greys and 24-bit colours are unchanged. `terminal_view.style_format()` picks the set from the view's palette: its Base lightness under 128 is dark. Checked that qt_material's stylesheet reaches a polished view's palette: `dark_amber.xml` gives Base `#31363b` (lightness 54), `light_blue.xml` `#e6e6e6` (230). A screenshot of the SSH widget on the dark theme shows the directory blue readable. +- **Tests**: `test_terminal_style.py`: the `colour_rgb` cases for both backgrounds (5 + 12), the view background picking the set (+2); the red expected in the format and widget tests is VS Code's (205, 49, 49), the same on both. +- **Result / numbers**: 2371 tests in 130 files. The terminal and SSH tests (96) pass. `ruff check` clean. +- **Docs**: `architecture_explore.md` (utils table, test count). +- **Files**: `pybreeze/utils/terminal_style.py`, `pybreeze/pybreeze_ui/terminal_view.py`, `test/test_utils/test_terminal_style.py`, `test/test_utils/test_ssh_terminal_output.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 36b7f24b..c8f2a6c4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-293 | 2026-09-24 | Terminal colours readable on a dark and on a light theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-292 | 2026-09-24 | Colours in the SSH terminal | #feature #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-291 | 2026-09-24 | The SSH shell's pty follows the terminal's size | #feature #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-290 | 2026-09-24 | Terminal output in a fixed-pitch font | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | @@ -374,4 +375,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 90 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 91 | diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py index efa4965f..24dd70d2 100644 --- a/pybreeze/pybreeze_ui/terminal_view.py +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -41,19 +41,27 @@ def terminal_size(view: QPlainTextEdit) -> tuple[int, int]: return max(columns, MIN_COLUMNS), max(rows, MIN_ROWS) -def _qcolour(colour: Colour | None) -> QColor | None: - return None if colour is None else QColor(*colour_rgb(colour)) +# Background lightness (0–255) below which a view counts as dark +_DARK_BELOW = 128 + + +def _qcolour(colour: Colour | None, on_dark: bool) -> QColor | None: + return None if colour is None else QColor(*colour_rgb(colour, on_dark=on_dark)) def style_format(style: TextStyle, palette: QPalette) -> QTextCharFormat: - """The format for text in *style*; *palette* gives the view's own colours, for inverse. + """The format for text in *style* in a view with *palette*. - The plain style is an empty format: the view's own font and colours. + The palette's background picks the dark or the light set of the 16 basic + colours, and its colours stand in for defaults in inverse. The plain style + is an empty format: the view's own font and colours. """ text_format = QTextCharFormat() if style == PLAIN: return text_format - foreground, background = _qcolour(style.foreground), _qcolour(style.background) + on_dark = palette.color(QPalette.ColorRole.Base).lightness() < _DARK_BELOW + foreground = _qcolour(style.foreground, on_dark) + background = _qcolour(style.background, on_dark) if style.inverse: foreground, background = (background or palette.color(QPalette.ColorRole.Base), foreground or palette.color(QPalette.ColorRole.Text)) diff --git a/pybreeze/utils/terminal_style.py b/pybreeze/utils/terminal_style.py index 770638e5..43a5ad07 100644 --- a/pybreeze/utils/terminal_style.py +++ b/pybreeze/utils/terminal_style.py @@ -34,20 +34,28 @@ _PALETTE_FORM = 5 # 38;5;n _RGB_FORM = 2 # 38;2;r;g;b -# xterm's first 16 colours: normal, then bright -_BASIC_COLOURS = ( - (0, 0, 0), (205, 0, 0), (0, 205, 0), (205, 205, 0), - (0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229), - (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), - (92, 92, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255), +# The first 16 colours (normal, then bright) as VS Code's terminal shows them +# on a dark and on a light theme. xterm's own blue (0, 0, 238) was all but +# unreadable on the IDE's dark background. +_ON_DARK = ( + (0, 0, 0), (205, 49, 49), (13, 188, 121), (229, 229, 16), + (36, 114, 200), (188, 63, 188), (17, 168, 205), (229, 229, 229), + (102, 102, 102), (241, 76, 76), (35, 209, 139), (245, 245, 67), + (59, 142, 234), (214, 112, 214), (41, 184, 219), (229, 229, 229), +) +_ON_LIGHT = ( + (0, 0, 0), (205, 49, 49), (16, 124, 16), (148, 152, 0), + (4, 81, 165), (188, 5, 188), (5, 152, 188), (85, 85, 85), + (102, 102, 102), (241, 76, 76), (20, 206, 20), (181, 186, 0), + (59, 142, 234), (214, 112, 214), (41, 184, 219), (165, 165, 165), ) _CUBE_LEVELS = (0, 95, 135, 175, 215, 255) _CUBE_START = 16 _GREY_START = 232 _PALETTE_SIZE = 256 _CHANNEL_MAX = 255 -# Longest parameter read: past it (a server can send thousands of digits, and -# int() refuses more than 4300) it is one no parameter means +# Longest parameter read: a longer one (a server can send thousands of digits, +# and int() refuses more than 4300) is taken as unknown _MAX_PARAMETER_DIGITS = 5 _UNKNOWN = -1 @@ -67,12 +75,17 @@ class TextStyle: PLAIN = TextStyle() -def colour_rgb(colour: Colour) -> tuple[int, int, int]: - """The (red, green, blue) of *colour*, as xterm shows it.""" +def colour_rgb(colour: Colour, *, on_dark: bool) -> tuple[int, int, int]: + """The (red, green, blue) of *colour*, on a dark background or a light one. + + The first 16 differ between the two; the rest of the palette (xterm's + colour cube and greys) and a colour given as red, green and blue do not. + """ if isinstance(colour, tuple): return colour - if colour < len(_BASIC_COLOURS): - return _BASIC_COLOURS[colour] + basic = _ON_DARK if on_dark else _ON_LIGHT + if colour < len(basic): + return basic[colour] if colour < _GREY_START: index = colour - _CUBE_START return (_CUBE_LEVELS[index // 36], _CUBE_LEVELS[index // 6 % 6], _CUBE_LEVELS[index % 6]) diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index f3ee6340..bde6ed9a 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -502,17 +502,17 @@ def test_output_shows_the_colours_it_asks_for(self, app): widget._on_data(b"\x1b[31mred\x1b[0m plain") assert widget.terminal.toPlainText() == "red plain" - assert _colour_at(widget, 0) == QColor(205, 0, 0) - assert _colour_at(widget, 4) != QColor(205, 0, 0) + assert _colour_at(widget, 0) == QColor(205, 49, 49) + assert _colour_at(widget, 4) != QColor(205, 49, 49) widget.close() def test_a_colour_carries_over_to_the_next_read(self, app): widget = SSHCommandWidget() - widget._on_data(b"\x1b[32mgr") - widget._on_data(b"een") + widget._on_data(b"\x1b[31mr") + widget._on_data(b"ed") - assert _colour_at(widget, 4) == QColor(0, 205, 0) + assert _colour_at(widget, 2) == QColor(205, 49, 49) # the same on any background widget.close() def test_a_new_session_starts_without_the_last_one_colour(self, app): diff --git a/test/test_utils/test_terminal_style.py b/test/test_utils/test_terminal_style.py index 957fbe82..4149419f 100644 --- a/test/test_utils/test_terminal_style.py +++ b/test/test_utils/test_terminal_style.py @@ -51,9 +51,17 @@ def test_a_parameter_thousands_of_digits_long_is_ignored(self): class TestColourRgb: + @pytest.mark.parametrize(("colour", "on_dark", "rgb"), [ + (4, True, (36, 114, 200)), # xterm's (0, 0, 238) was unreadable on a dark theme + (4, False, (4, 81, 165)), + (7, True, (229, 229, 229)), + (7, False, (85, 85, 85)), # "white" text stays readable on white + (1, True, (205, 49, 49)), + ]) + def test_the_basic_colours_suit_the_background(self, colour, on_dark, rgb): + assert colour_rgb(colour, on_dark=on_dark) == rgb + @pytest.mark.parametrize(("colour", "rgb"), [ - (1, (205, 0, 0)), - (15, (255, 255, 255)), (16, (0, 0, 0)), (196, (255, 0, 0)), (231, (255, 255, 255)), @@ -61,8 +69,9 @@ class TestColourRgb: (255, (238, 238, 238)), ((1, 2, 3), (1, 2, 3)), ]) - def test_as_xterm_shows_it(self, colour, rgb): - assert colour_rgb(colour) == rgb + @pytest.mark.parametrize("on_dark", [True, False]) + def test_the_rest_of_the_palette_is_xterm_on_any_background(self, colour, rgb, on_dark): + assert colour_rgb(colour, on_dark=on_dark) == rgb class TestSplitStyled: @@ -100,7 +109,7 @@ def test_the_plain_style_changes_nothing(self, palette): def test_colours_and_emphasis(self, palette): text_format = style_format(TextStyle(foreground=1, background=(1, 2, 3), bold=True, underline=True), palette) - assert text_format.foreground().color() == QColor(205, 0, 0) + assert text_format.foreground().color() == QColor(205, 49, 49) assert text_format.background().color() == QColor(1, 2, 3) assert text_format.fontWeight() == QFont.Weight.Bold assert text_format.fontUnderline() @@ -109,4 +118,20 @@ def test_inverse_swaps_the_view_colours_in_for_defaults(self, palette): text_format = style_format(TextStyle(foreground=1, inverse=True), palette) assert text_format.foreground().color() == palette.color(QPalette.ColorRole.Base) - assert text_format.background().color() == QColor(205, 0, 0) + assert text_format.background().color() == QColor(205, 49, 49) + + +def _palette_with_base(colour: QColor) -> QPalette: + palette = QPalette() + palette.setColor(QPalette.ColorRole.Base, colour) + return palette + + +@pytest.mark.parametrize(("base", "blue"), [ + (QColor(30, 30, 30), QColor(36, 114, 200)), + (QColor(255, 255, 255), QColor(4, 81, 165)), +]) +def test_the_view_background_picks_the_basic_colours(palette, base, blue): + text_format = style_format(TextStyle(foreground=4), _palette_with_base(base)) + + assert text_format.foreground().color() == blue From 8c0ed5f76ef396422e114aaaebaead01dee7aba8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 19:59:06 +0800 Subject: [PATCH 024/166] Keep the terminal font under a theme's style sheet --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 11 +++++++++++ docs/updates/README.md | 3 ++- pybreeze/pybreeze_ui/terminal_view.py | 9 ++++++++- test/test_utils/test_terminal_font.py | 16 +++++++++++++++- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 9a7d19f9..a835004f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2371 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2372 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 1617f2c7..74a64386 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -848,3 +848,14 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `architecture_explore.md` (utils table, test count). - **Files**: `pybreeze/utils/terminal_style.py`, `pybreeze/pybreeze_ui/terminal_view.py`, `test/test_utils/test_terminal_style.py`, `test/test_utils/test_ssh_terminal_output.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-294 · 2026-09-24 · The terminal font holds under the IDE's theme · #fix #ssh #ui + +- **What**: U-20260924-290 gave the SSH terminal and the run window a fixed-pitch font with `setFont`. The IDE applies a qt_material theme (`ui_style`, `dark_amber.xml` by default), whose style sheet names a font for every widget (`* { font-family: Roboto }`), and a style sheet's font overrides `setFont`: in the themed IDE both views were still Roboto, proportional. The tests ran without a theme, so they passed. Found rendering the views with `apply_stylesheet(app, "dark_amber.xml")`. +- **Fix**: `use_terminal_font` also sets the family in the view's own style sheet, which outranks the application's; the theme's size (in pixels) stays. A size of -1 (a sheet gave pixels) is no longer passed to `setPointSizeF`. +- **Check**: with `dark_amber.xml` both views are Courier New, fixed pitch, at the theme's 13 px; `terminal_size` counts 106 x 18 for a 900 x 520 SSH tab; a screenshot shows `ls -l` columns aligned. Without a theme: Courier New at 9 pt, as before. +- **Tests**: `test_terminal_font.py` +1: a parent's `* { font-family: "Arial" }` does not take the font back (it came back Arial with `setFont` alone). +- **Result / numbers**: 2372 tests in 130 files. The terminal, run-window and SSH tests (103) pass. `ruff check` clean. +- **Docs**: `architecture_explore.md` (test count). +- **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `test/test_utils/test_terminal_font.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index c8f2a6c4..ebd340ba 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-294 | 2026-09-24 | The terminal font holds under the IDE's theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-293 | 2026-09-24 | Terminal colours readable on a dark and on a light theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-292 | 2026-09-24 | Colours in the SSH terminal | #feature #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-291 | 2026-09-24 | The SSH shell's pty follows the terminal's size | #feature #ssh | [2026-09-b](2026-09-b.md) | @@ -375,4 +376,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 91 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 92 | diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py index 24dd70d2..d3668349 100644 --- a/pybreeze/pybreeze_ui/terminal_view.py +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -16,10 +16,17 @@ def use_terminal_font(view: QPlainTextEdit) -> None: Output laid out in columns (``ls -l``, ``df``, a table a script prints) lines up only when every character is as wide as the next; in the interface's proportional font it came out ragged. + + The family is also set in the view's own style sheet: a theme's sheet + (qt_material names a font for every widget) overrides ``setFont``, and + the view's sheet overrides the application's. The theme's size stays. """ font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) - font.setPointSizeF(view.font().pointSizeF()) + size = view.font().pointSizeF() + if size > 0: # -1 when a style sheet gave the size in pixels + font.setPointSizeF(size) view.setFont(font) + view.setStyleSheet(f'font-family: "{font.family()}";') # Smallest size given to a program for a view squeezed to almost nothing diff --git a/test/test_utils/test_terminal_font.py b/test/test_utils/test_terminal_font.py index a9bb1043..8bf2e614 100644 --- a/test/test_utils/test_terminal_font.py +++ b/test/test_utils/test_terminal_font.py @@ -7,7 +7,7 @@ import pytest from PySide6.QtGui import QFontDatabase -from PySide6.QtWidgets import QApplication, QPlainTextEdit +from PySide6.QtWidgets import QApplication, QPlainTextEdit, QWidget from pybreeze.extend_multi_language.update_language_dict import update_language_dict from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget @@ -38,6 +38,20 @@ def test_the_view_gets_the_fixed_pitch_font_at_its_own_size(app): assert view.font().pointSizeF() == 13.5 +def test_a_theme_style_sheet_does_not_take_the_font_back(app): + # qt_material names a font for every widget ("* { font-family: Roboto }"), + # and a style sheet's font overrides setFont: the IDE showed Roboto + theme = QWidget() + theme.setStyleSheet('* { font-family: "Arial"; }') + view = QPlainTextEdit(theme) + use_terminal_font(view) + + view.ensurePolished() + + assert view.font().family() == _fixed_family() + theme.close() + + def test_the_ssh_terminal_shows_a_fixed_pitch_font(app): widget = SSHCommandWidget() try: From 567e1a0776560b0f7af9ae02a3f1502b8b043363 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 20:03:22 +0800 Subject: [PATCH 025/166] Colour automation keywords from the theme --- architecture.md | 4 +- architecture_explore.md | 4 +- docs/updates/2026-09-b.md | 10 +++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/syntax/syntax_extend.py | 12 ++++-- test/test_utils/test_jeditor_contract.py | 11 ++++++ test/test_utils/test_syntax_extend.py | 39 ++++++++++++++++++++ 7 files changed, 76 insertions(+), 7 deletions(-) diff --git a/architecture.md b/architecture.md index 15abc2c2..a286578a 100644 --- a/architecture.md +++ b/architecture.md @@ -150,7 +150,9 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() `_ignore_next_change`, `_is_modified` and `_on_text_changed()` (a rename puts the unsaved mark back after `rename_self_tab()` clears it), on `EditorMain.close_tab(index)` (overridden to ask a tab's `may_close()` first, and to delete a closed tool tab, which its `removeTab` keeps) and on `CodeEditor`'s `reset_highlighter()`, `load_git_baseline()` and - `start_language_server()` (a rename moves the tab the way `open_an_file` does), and on `language_wrapper`'s + `start_language_server()` (a rename moves the tab the way `open_an_file` does), on the syntax highlighter + taking a theme colour key (`warning_output_color`, `diff_modified_marker_color`, in both JEditor's dark and light + sets) for a registered keyword's colour (`syntax/syntax_extend.py`), and on `language_wrapper`'s `choose_language_dict` serving English and Traditional Chinese from the exported dict objects themselves. Having je_editor export the names in the table is workspace X-17. It merges its strings by mutating JEditor's `english_word_dict` and `traditional_chinese_word_dict` diff --git a/architecture_explore.md b/architecture_explore.md index a835004f..f9908183 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -330,7 +330,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 - `syntax_keyword.py`(629 行)— 七份關鍵字清單,彙整成 `package_keyword_list`: `je_auto_control` / `je_load_density` / `je_api_testka` / `je_web_runner` / `automation_file` / `mail_thunder` / `test_pioneer` -- `syntax_extend.py` — 把前六個註冊到 `.json`(黃色 `#FFFF00`),`test_pioneer` 註冊到 `TEST_PIONEER_SUFFIXES` 的每個副檔名(`.yml`、`.yaml`,橘色 `#FF9900`),然後重置當前編輯器的 highlighter +- `syntax_extend.py` — 把前六個註冊到 `.json`(黃色,JEditor 主題色鍵 `warning_output_color`),`test_pioneer` 註冊到 `TEST_PIONEER_SUFFIXES` 的每個副檔名(`.yml`、`.yaml`,橘色,`diff_modified_marker_color`);顏色給的是主題色的鍵而不是固定顏色,JEditor 的 highlighter 每次建立時查 `actually_color_dict`,所以深色與淺色主題各用各的一組,然後重置當前編輯器的 highlighter `PackageManager.syntax_check_list` 決定要註冊哪些;用 `package_keyword_list.get(pkg, [])` 取值,套件沒有關鍵字清單時註冊空集合而不是炸掉。 @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2372 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2375 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 74a64386..6d361fba 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -859,3 +859,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `architecture_explore.md` (test count). - **Files**: `pybreeze/pybreeze_ui/terminal_view.py`, `test/test_utils/test_terminal_font.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-295 · 2026-09-24 · Automation keywords readable on a light theme · #fix #ui #jeditor + +- **What**: `syntax_extend_package` registered the automation keywords with fixed colours: pure yellow (255, 255, 0) in `.json` files, orange (255, 153, 0) in TestPioneer `.yml`/`.yaml`. JEditor has a light set of its own colours for the light qt_material styles (`je_editor/utils/theme/theme_colors.py`), but these two stayed fixed, and yellow text on a light editor background has almost no contrast. Found looking for colours the theme cannot reach after U-20260924-294. +- **Fix**: the keywords are registered with JEditor theme colour keys instead: `JSON_KEYWORD_COLOUR = "warning_output_color"` (dark set (204, 204, 0), light set (150, 120, 0)) and `YAML_KEYWORD_COLOUR = "diff_modified_marker_color"` ((255, 167, 38) / (230, 126, 0)). JEditor's highlighter looks a string colour up in `actually_color_dict` each time it is built (je-editor 1.0.27, the lowest PyBreeze accepts; checked in the published wheel), and the Style menu rebuilds the highlighters, so a theme change reaches them. +- **Tests**: `test_syntax_extend.py` +2 (a `.json` and a `.yaml` keyword take the theme's colour on dark_amber and on light_blue, and the two differ; both failed on the fixed colours), `test_jeditor_contract.py` +1 (the highlighter takes a key, and both keys are in the dark and the light set). +- **Result / numbers**: 2375 tests in 130 files. Full suite at d261735: 2356 passed, 15 skipped; both startup tests exit 0. `ruff check` clean. +- **Docs**: `architecture.md` §6 (the reliance on JEditor), `architecture_explore.md` (§11, test count). +- **Files**: `pybreeze/pybreeze_ui/syntax/syntax_extend.py`, `test/test_utils/test_syntax_extend.py`, `test/test_utils/test_jeditor_contract.py`, `architecture.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index ebd340ba..2d70ff7c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-295 | 2026-09-24 | Automation keywords readable on a light theme | #fix #ui #jeditor | [2026-09-b](2026-09-b.md) | | U-20260924-294 | 2026-09-24 | The terminal font holds under the IDE's theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-293 | 2026-09-24 | Terminal colours readable on a dark and on a light theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-292 | 2026-09-24 | Colours in the SSH terminal | #feature #ssh #ui | [2026-09-b](2026-09-b.md) | @@ -376,4 +377,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 92 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 93 | diff --git a/pybreeze/pybreeze_ui/syntax/syntax_extend.py b/pybreeze/pybreeze_ui/syntax/syntax_extend.py index 04734458..79ad5958 100644 --- a/pybreeze/pybreeze_ui/syntax/syntax_extend.py +++ b/pybreeze/pybreeze_ui/syntax/syntax_extend.py @@ -6,11 +6,17 @@ if TYPE_CHECKING: from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow -from PySide6.QtGui import QColor from pybreeze.pybreeze_ui.syntax.syntax_keyword import TEST_PIONEER_SUFFIXES, package_keyword_list from pybreeze.utils.manager.package_manager.package_manager_class import package_manager +# Keyword colours as keys into JEditor's theme colours, which have a dark and a +# light set: fixed yellow (255, 255, 0) keywords could not be read on a light +# theme. The highlighter looks the key up each time it is built, so the +# colours follow a theme change. +JSON_KEYWORD_COLOUR = "warning_output_color" # yellow; dark yellow on light +YAML_KEYWORD_COLOUR = "diff_modified_marker_color" # orange + def syntax_extend_package(main_window: PyBreezeMainWindow) -> None: # Register JSON syntax keywords for each automation package @@ -20,7 +26,7 @@ def syntax_extend_package(main_window: PyBreezeMainWindow) -> None: # no words instead of crashing syntax setup with set(None). json_syntax_words[package] = { "words": set(package_keyword_list.get(package, [])), - "color": QColor(255, 255, 0), + "color": JSON_KEYWORD_COLOUR, } register_programming_language(".json", json_syntax_words) @@ -29,7 +35,7 @@ def syntax_extend_package(main_window: PyBreezeMainWindow) -> None: yml_syntax_words = { "test_pioneer": { "words": set(package_keyword_list.get("test_pioneer", [])), - "color": QColor(255, 153, 0), + "color": YAML_KEYWORD_COLOUR, } } for suffix in TEST_PIONEER_SUFFIXES: diff --git a/test/test_utils/test_jeditor_contract.py b/test/test_utils/test_jeditor_contract.py index af4d3b97..e0817481 100644 --- a/test/test_utils/test_jeditor_contract.py +++ b/test/test_utils/test_jeditor_contract.py @@ -114,6 +114,17 @@ def test_the_output_colours_are_there(self): "actually_color_dict") assert {"normal_output_color", "error_output_color"} <= set(colours) + def test_a_keyword_colour_can_be_a_theme_colour_key(self): + # syntax_extend registers keys, not colours, so keywords follow the theme + from je_editor.pyside_ui.code.syntax.python_syntax import PythonHighlighter + from je_editor.utils.theme.theme_colors import DARK_COLORS, LIGHT_COLORS + + from pybreeze.pybreeze_ui.syntax.syntax_extend import JSON_KEYWORD_COLOUR, YAML_KEYWORD_COLOUR + + assert "actually_color_dict.get(color)" in inspect.getsource(PythonHighlighter._make_format) + for key in (JSON_KEYWORD_COLOUR, YAML_KEYWORD_COLOUR): + assert key in DARK_COLORS and key in LIGHT_COLORS, key + def test_the_widgets_build_without_arguments(self): from PySide6.QtWidgets import QDockWidget, QWidget diff --git a/test/test_utils/test_syntax_extend.py b/test/test_utils/test_syntax_extend.py index af34d80b..55d3bfb1 100644 --- a/test/test_utils/test_syntax_extend.py +++ b/test/test_utils/test_syntax_extend.py @@ -49,3 +49,42 @@ def test_every_automation_package_gets_json_keywords(registered): groups = get_programming_language_plugin(".json")["syntax_words"] assert set(groups) == set(package_manager.syntax_check_list) + + +class _File: + def __init__(self, name: str) -> None: + self.current_file = name + + +def _keyword_colour(suffix: str, keyword: str): + from je_editor.pyside_ui.code.syntax.python_syntax import PythonHighlighter + from PySide6.QtCore import QRegularExpression + from PySide6.QtWidgets import QApplication + + QApplication.instance() or QApplication([]) + highlighter = PythonHighlighter(main_window=_File(f"script{suffix}")) + pattern = QRegularExpression(rf"\b{keyword}\b").pattern() + return next(fmt.foreground().color() for rule, fmt in highlighter.highlight_rules if rule.pattern() == pattern) + + +@pytest.mark.parametrize(("suffix", "package", "key"), [ + (".json", "je_auto_control", "warning_output_color"), + (".yaml", "test_pioneer", "diff_modified_marker_color"), +]) +def test_keywords_take_the_theme_colour_and_follow_a_light_theme(registered, suffix, package, key): + # Fixed pure yellow keywords could not be read on a light theme + from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import ( + actually_color_dict, apply_theme_colors, + ) + + keyword = sorted(package_keyword_list[package])[0] + try: + apply_theme_colors("dark_amber.xml") + on_dark = _keyword_colour(suffix, keyword) + apply_theme_colors("light_blue.xml") + on_light = _keyword_colour(suffix, keyword) + assert on_light == actually_color_dict[key] + finally: + apply_theme_colors("dark_amber.xml") + + assert on_dark != on_light From 36aff713d5e32a576402b3917a1c840b504b2223 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 21:04:37 +0800 Subject: [PATCH 026/166] Colour the diff tool's added and removed lines --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture.md | 2 +- architecture_explore.md | 4 +-- docs/updates/2026-09-b.md | 11 ++++++ docs/updates/README.md | 3 +- images/tool_diff.png | Bin 32907 -> 22727 bytes pybreeze/pybreeze_ui/tools_gui/diff_gui.py | 37 +++++++++++++++++++++ test/test_utils/test_diff_gui.py | 37 +++++++++++++++++++++ test/test_utils/test_jeditor_contract.py | 5 ++- 11 files changed, 97 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0b2c430c..9a70cb6c 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Reports names sent more than once, `Set-Cookie` entries missing `Secure` / `Http ![Text Diff](images/tool_diff.png) -Compare two payloads — an expected vs. actual API response, say — and get a unified diff plus a one-line added/removed summary. +Compare two payloads — an expected vs. actual API response, say — and get a unified diff, its added and removed lines in the theme's colours, plus a one-line added/removed summary. ### The everyday utilities diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index afa5522d..1308cb7b 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -117,7 +117,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![文本比较](../images/tool_diff.png) -比较两份数据——例如预期与实际的 API 响应——得到 unified diff,以及一行新增/删除的摘要。 +比较两份数据——例如预期与实际的 API 响应——得到 unified diff(新增与删除的行以主题的颜色标示),以及一行新增/删除的摘要。 ### 日常小工具 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 2243903a..b999a2eb 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -117,7 +117,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![文字比對](../images/tool_diff.png) -比較兩段內容——例如預期與實際的 API 回應——得到 unified diff,以及一行新增/刪除摘要。 +比較兩段內容——例如預期與實際的 API 回應——得到 unified diff(新增與刪除的行以主題的顏色標示),以及一行新增/刪除摘要。 ### 日常小工具 diff --git a/architecture.md b/architecture.md index a286578a..0c7feb83 100644 --- a/architecture.md +++ b/architecture.md @@ -143,7 +143,7 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() | `choose_file_get_save_file_path` | `pyside_ui.dialog.file_dialog.save_file_dialog` | `menu/plugin_menu/build_run_with_menu.py` | | `write_file_with_encoding` | `utils.file.save.save_file` | `menu/plugin_menu/build_run_with_menu.py` | | `DEFAULT_ENCODING`, `LINE_ENDING_LF` | `utils.encodings.text_codec` | `menu/plugin_menu/build_run_with_menu.py` | - | `actually_color_dict` | `pyside_ui.main_ui.save_settings.user_color_setting_file` | `show_code_window/code_window.py`, `automation_menu/auto_control_menu/build_autocontrol_menu.py` | + | `actually_color_dict` | `pyside_ui.main_ui.save_settings.user_color_setting_file` | `show_code_window/code_window.py`, `automation_menu/auto_control_menu/build_autocontrol_menu.py`, `tools_gui/diff_gui.py` (the diff's line colours: `diff_added_marker_color`, `diff_removed_marker_color`, `syntax_keyword_color`, `blame_annotation_color`) | PyBreeze also relies on `EditorWidget`'s `current_file`, `code_edit`, `file_encoding`, `line_ending`, `mark_ignore_next_file_change()` and `mark_saved()`, on its private `_file_watcher`, diff --git a/architecture_explore.md b/architecture_explore.md index f9908183..970398a9 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -227,7 +227,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) | `UrlBuilderGUI` | `utils/url_tools/` | URL 拆成 JSON 元件 / 由元件組回 URL | | `RegexGUI` | `utils/regex_tools/` | regex 測試,flag 勾選、列出每個 match 與群組。pattern 在另一個行程裡跑(`find_matches_bounded()`:從原始碼執行時是 `python -I -S -c` 跑一段只用標準函式庫的固定腳本,工作用 JSON 從 stdin 進、結果從 stdout 出,5 秒後 kill;打包版沒有直譯器可用,仍是 multiprocessing spawn),分頁用 `RegexMatchThread` 等它:`re` 開始比對後就停不下來,災難性回溯只有整個行程能停。spawn 會重新匯入啟動 IDE 的腳本,README 那種沒有 `__main__` 防護的腳本會每跑一次就再開一個 IDE。還在跑的 worker 記在 `_RUNNING`,分頁關閉時 `stop_running_workers()` 結束它(IDE 以 `os._exit` 結束,子行程不會跟著走);執行中不能存檔,列到 `MAX_MATCHES` 上限時會註明可能還有更多 | | `HttpStatusGUI` | `utils/http_reference/` | 狀態碼參考,可依碼前綴或描述搜尋 | -| `DiffGUI` | `utils/diff_tools/` | unified diff + 增刪統計,`compare_texts()` 的統計與 diff 共用同一次比對(`_TrimmedMatcher`:先把相同的開頭結尾放一邊再比對中間,長而重複的文字改一行就是一行;放一邊有時反而比對得更差(`b b a b a` 對 `b a c b`),所以有放一邊、且兩段合計不超過 2,000 行時,`_closest_match` 也照原樣比一次,取改動行數少的(更大的文字再比一次會讓等待加倍);autojunk 照 difflib 的預設,關掉的話重複的文字比對時間隨行數平方成長;diff 照 `difflib.unified_diff` 的格式從它的 grouped opcodes 寫出),在 `DiffThread` 上算、不佔 UI 執行緒(4 萬行要四秒多),比對中按鈕停用、關閉時交給 `let_run_out()`。逐行比不出差別、文字卻不同時(最後少一個換行、`\r\n` 對 `\n`),改成連行尾一起比:少換行的那行下面標 `\ No newline at end of file`,其他行尾寫出來 | +| `DiffGUI` | `utils/diff_tools/` | unified diff + 增刪統計,`compare_texts()` 的統計與 diff 共用同一次比對(`_TrimmedMatcher`:先把相同的開頭結尾放一邊再比對中間,長而重複的文字改一行就是一行;放一邊有時反而比對得更差(`b b a b a` 對 `b a c b`),所以有放一邊、且兩段合計不超過 2,000 行時,`_closest_match` 也照原樣比一次,取改動行數少的(更大的文字再比一次會讓等待加倍);autojunk 照 difflib 的預設,關掉的話重複的文字比對時間隨行數平方成長;diff 照 `difflib.unified_diff` 的格式從它的 grouped opcodes 寫出),在 `DiffThread` 上算、不佔 UI 執行緒(4 萬行要四秒多),比對中按鈕停用、關閉時交給 `let_run_out()`。逐行比不出差別、文字卻不同時(最後少一個換行、`\r\n` 對 `\n`),改成連行尾一起比:少換行的那行下面標 `\ No newline at end of file`,其他行尾寫出來。輸出由 `UnifiedDiffHighlighter` 依行首上色(`diff_line_colour()`:`@@`、`+`、`-`、`\\ No newline` 各用 JEditor 的主題色,深色淺色各一組;只有前兩行算 `---`/`+++` 標頭) | | `JsonFormatGUI` | `utils/json_format/` | 美化 / 壓縮 / 驗證 | | `HeaderAnalyzerGUI` | `utils/header_tools/` | HTTP header 安全稽核(HSTS、CSP、CORS、Set-Cookie、banner…)| | `ResponseInspectorGUI` | `utils/response_inspector/` | 貼整包 response → 拆狀態列/headers/body,順便挖出 JWT;`curl -i` 印出的多段回應(`100 Continue`、proxy 的 `Connection established`、`-L` 的轉址)取最後一段;只有一行又沒有狀態列就當 body | @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2375 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2386 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 6d361fba..ed8e6f0a 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -869,3 +869,14 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `architecture.md` §6 (the reliance on JEditor), `architecture_explore.md` (§11, test count). - **Files**: `pybreeze/pybreeze_ui/syntax/syntax_extend.py`, `test/test_utils/test_syntax_extend.py`, `test/test_utils/test_jeditor_contract.py`, `architecture.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-296 · 2026-09-24 · The diff tool shows its diff in colour · #feature #ui + +- **What**: the Text Diff tab printed its unified diff in one colour; added and removed lines could be told apart only by their first character. Seen on the dark_amber contact sheet of all 20 tool tabs (the rest read fine under the theme). +- **Change**: `diff_gui.UnifiedDiffHighlighter` (a `QSyntaxHighlighter` on the output) colours a line by its start: `@@` hunks `syntax_keyword_color`, `+` `diff_added_marker_color`, `-` `diff_removed_marker_color`, the `\ No newline at end of file` note `blame_annotation_color`, all JEditor theme colours with a dark and a light set (`actually_color_dict`, already used by the run window). `diff_line_colour(line, number)` decides; the `--- expected` / `+++ actual` header is only the first two lines, since a removed `--x` also starts with `---`. Copy and Save still take the plain text. Highlighting an 18,000-line diff adds 0.14 s to showing it. +- **Check**: rendered on dark_amber and light_blue: readable on both. +- **Tests**: `test_diff_gui.py` +11 (each kind of line, including `---x` and `+++x` past the header; the widget shows `-b` and `+c` in the removed and added colours and context in none), `test_jeditor_contract.py` (the four colour keys are in `actually_color_dict`; `diff_gui.py` listed as a user). +- **Result / numbers**: 2386 tests in 130 files. The diff and contract tests (57) pass. `ruff check` clean. +- **Docs**: the three READMEs' Text Diff section and its screenshot `images/tool_diff.png` (same sample and size, now in colour), `architecture.md` §6 (`actually_color_dict` users), `architecture_explore.md` (diff tool, test count). +- **Files**: `pybreeze/pybreeze_ui/tools_gui/diff_gui.py`, `test/test_utils/test_diff_gui.py`, `test/test_utils/test_jeditor_contract.py`, `images/tool_diff.png`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 2d70ff7c..98021c6a 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-296 | 2026-09-24 | The diff tool shows its diff in colour | #feature #ui | [2026-09-b](2026-09-b.md) | | U-20260924-295 | 2026-09-24 | Automation keywords readable on a light theme | #fix #ui #jeditor | [2026-09-b](2026-09-b.md) | | U-20260924-294 | 2026-09-24 | The terminal font holds under the IDE's theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | | U-20260924-293 | 2026-09-24 | Terminal colours readable on a dark and on a light theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | @@ -377,4 +378,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 93 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 94 | diff --git a/images/tool_diff.png b/images/tool_diff.png index ebc87b2c4bd4090c13f0dfff78f49cc00a33a90a..6f57b2b834b6df3c54dc96a525040a16975dac81 100644 GIT binary patch literal 22727 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℘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 JEditor theme colour (a dark and a +# light set, which the Style menu switches between); the first match counts +_LINE_COLOURS = ( + ("@@", "syntax_keyword_color"), + ("+", "diff_added_marker_color"), + ("-", "diff_removed_marker_color"), + ("\\", "blame_annotation_color"), # "\ No newline at end of file" +) +# The "--- expected" and "+++ actual" lines a diff starts with +_HEADER_LINES = 2 + + +def diff_line_colour(line: str, line_number: int) -> str | None: + """The theme colour key *line* of a unified diff is shown in, or ``None`` for the view's own. + + Only the first two lines are the header: a removed line reading ``--x`` + also starts with ``---``. + """ + if line_number < _HEADER_LINES and line.startswith(("--- ", "+++ ")): + return None + return next((key for prefix, key in _LINE_COLOURS if line.startswith(prefix)), None) + + +class UnifiedDiffHighlighter(QSyntaxHighlighter): + """Colours a unified diff's added, removed and hunk lines, in the theme's colours.""" + + def highlightBlock(self, text: str) -> None: + key = diff_line_colour(text, self.currentBlock().blockNumber()) + colour = actually_color_dict.get(key) if key is not None else None + if colour is not None: + text_format = QTextCharFormat() + text_format.setForeground(colour) + self.setFormat(0, len(text), text_format) + def build_summary_line(summary: DiffSummary) -> str: """Render a one-line summary of a diff. @@ -72,6 +108,7 @@ def __init__(self, main_window=None) -> None: self.summary_label = QLabel("") self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) + self._highlighter = UnifiedDiffHighlighter(self.output_edit.document()) self.actions = OutputActions( self, self.output_edit, main_window=main_window, diff --git a/test/test_utils/test_diff_gui.py b/test/test_utils/test_diff_gui.py index 42df3fe3..ba4a83ba 100644 --- a/test/test_utils/test_diff_gui.py +++ b/test/test_utils/test_diff_gui.py @@ -114,3 +114,40 @@ def test_a_second_compare_while_one_runs_is_ignored(self, widget, monkeypatch): release.set() _compare(widget, start=False) assert "+b" in widget.output_edit.toPlainText() + + +class TestTheColours: + @pytest.mark.parametrize(("line", "number", "key"), [ + ("--- expected", 0, None), + ("+++ actual", 1, None), + ("@@ -1,2 +1,2 @@", 2, "syntax_keyword_color"), + ("+added", 5, "diff_added_marker_color"), + ("-removed", 5, "diff_removed_marker_color"), + ("---x", 7, "diff_removed_marker_color"), # a removed "--x", not a header + ("+++x", 7, "diff_added_marker_color"), + (" unchanged", 5, None), + ("\\ No newline at end of file", 6, "blame_annotation_color"), + ("", 5, None), + ]) + def test_each_kind_of_line_has_its_theme_colour(self, line, number, key): + from pybreeze.pybreeze_ui.tools_gui.diff_gui import diff_line_colour + + assert diff_line_colour(line, number) == key + + def test_the_diff_is_shown_in_colour(self, widget): + # It was all one colour: added and removed lines had to be read by their sign + from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict + + widget.left_edit.setPlainText("a\nb") + widget.right_edit.setPlainText("a\nc") + _compare(widget) + + colours = {} + block = widget.output_edit.document().begin() + while block.isValid(): + ranges = block.layout().formats() + colours[block.text()] = ranges[0].format.foreground().color() if ranges else None + block = block.next() + assert colours["-b"] == actually_color_dict["diff_removed_marker_color"] + assert colours["+c"] == actually_color_dict["diff_added_marker_color"] + assert colours[" a"] is None diff --git a/test/test_utils/test_jeditor_contract.py b/test/test_utils/test_jeditor_contract.py index e0817481..55517971 100644 --- a/test/test_utils/test_jeditor_contract.py +++ b/test/test_utils/test_jeditor_contract.py @@ -43,7 +43,7 @@ ("je_editor.utils.encodings.text_codec", "LINE_ENDING_LF", "menu/plugin_menu/build_run_with_menu.py"), ("je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file", "actually_color_dict", - "show_code_window/code_window.py, auto_control_menu/build_autocontrol_menu.py"), + "show_code_window/code_window.py, auto_control_menu/build_autocontrol_menu.py, tools_gui/diff_gui.py"), ] # Names PyBreeze imports from je_editor's top level, i.e. from its __all__. @@ -113,6 +113,9 @@ def test_the_output_colours_are_there(self): "je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file", "actually_color_dict") assert {"normal_output_color", "error_output_color"} <= set(colours) + # The diff tool's line colours + assert {"diff_added_marker_color", "diff_removed_marker_color", "syntax_keyword_color", + "blame_annotation_color"} <= set(colours) def test_a_keyword_colour_can_be_a_theme_colour_key(self): # syntax_extend registers keys, not colours, so keywords follow the theme From 800f557c562ffc28a4f5549b61507b29a570cd71 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 21:05:19 +0800 Subject: [PATCH 027/166] Fix a doubled backslash in the diff tool's map entry --- architecture_explore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/architecture_explore.md b/architecture_explore.md index 970398a9..77dbf011 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -227,7 +227,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) | `UrlBuilderGUI` | `utils/url_tools/` | URL 拆成 JSON 元件 / 由元件組回 URL | | `RegexGUI` | `utils/regex_tools/` | regex 測試,flag 勾選、列出每個 match 與群組。pattern 在另一個行程裡跑(`find_matches_bounded()`:從原始碼執行時是 `python -I -S -c` 跑一段只用標準函式庫的固定腳本,工作用 JSON 從 stdin 進、結果從 stdout 出,5 秒後 kill;打包版沒有直譯器可用,仍是 multiprocessing spawn),分頁用 `RegexMatchThread` 等它:`re` 開始比對後就停不下來,災難性回溯只有整個行程能停。spawn 會重新匯入啟動 IDE 的腳本,README 那種沒有 `__main__` 防護的腳本會每跑一次就再開一個 IDE。還在跑的 worker 記在 `_RUNNING`,分頁關閉時 `stop_running_workers()` 結束它(IDE 以 `os._exit` 結束,子行程不會跟著走);執行中不能存檔,列到 `MAX_MATCHES` 上限時會註明可能還有更多 | | `HttpStatusGUI` | `utils/http_reference/` | 狀態碼參考,可依碼前綴或描述搜尋 | -| `DiffGUI` | `utils/diff_tools/` | unified diff + 增刪統計,`compare_texts()` 的統計與 diff 共用同一次比對(`_TrimmedMatcher`:先把相同的開頭結尾放一邊再比對中間,長而重複的文字改一行就是一行;放一邊有時反而比對得更差(`b b a b a` 對 `b a c b`),所以有放一邊、且兩段合計不超過 2,000 行時,`_closest_match` 也照原樣比一次,取改動行數少的(更大的文字再比一次會讓等待加倍);autojunk 照 difflib 的預設,關掉的話重複的文字比對時間隨行數平方成長;diff 照 `difflib.unified_diff` 的格式從它的 grouped opcodes 寫出),在 `DiffThread` 上算、不佔 UI 執行緒(4 萬行要四秒多),比對中按鈕停用、關閉時交給 `let_run_out()`。逐行比不出差別、文字卻不同時(最後少一個換行、`\r\n` 對 `\n`),改成連行尾一起比:少換行的那行下面標 `\ No newline at end of file`,其他行尾寫出來。輸出由 `UnifiedDiffHighlighter` 依行首上色(`diff_line_colour()`:`@@`、`+`、`-`、`\\ No newline` 各用 JEditor 的主題色,深色淺色各一組;只有前兩行算 `---`/`+++` 標頭) | +| `DiffGUI` | `utils/diff_tools/` | unified diff + 增刪統計,`compare_texts()` 的統計與 diff 共用同一次比對(`_TrimmedMatcher`:先把相同的開頭結尾放一邊再比對中間,長而重複的文字改一行就是一行;放一邊有時反而比對得更差(`b b a b a` 對 `b a c b`),所以有放一邊、且兩段合計不超過 2,000 行時,`_closest_match` 也照原樣比一次,取改動行數少的(更大的文字再比一次會讓等待加倍);autojunk 照 difflib 的預設,關掉的話重複的文字比對時間隨行數平方成長;diff 照 `difflib.unified_diff` 的格式從它的 grouped opcodes 寫出),在 `DiffThread` 上算、不佔 UI 執行緒(4 萬行要四秒多),比對中按鈕停用、關閉時交給 `let_run_out()`。逐行比不出差別、文字卻不同時(最後少一個換行、`\r\n` 對 `\n`),改成連行尾一起比:少換行的那行下面標 `\ No newline at end of file`,其他行尾寫出來。輸出由 `UnifiedDiffHighlighter` 依行首上色(`diff_line_colour()`:`@@`、`+`、`-`、`\ No newline` 各用 JEditor 的主題色,深色淺色各一組;只有前兩行算 `---`/`+++` 標頭) | | `JsonFormatGUI` | `utils/json_format/` | 美化 / 壓縮 / 驗證 | | `HeaderAnalyzerGUI` | `utils/header_tools/` | HTTP header 安全稽核(HSTS、CSP、CORS、Set-Cookie、banner…)| | `ResponseInspectorGUI` | `utils/response_inspector/` | 貼整包 response → 拆狀態列/headers/body,順便挖出 JWT;`curl -i` 印出的多段回應(`100 Continue`、proxy 的 `Connection established`、`-L` 的轉址)取最後一段;只有一行又沒有狀態列就當 body | From 3d8cbcce9c547ae1e461981d3fdc62aa65409c76 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 21:13:58 +0800 Subject: [PATCH 028/166] Redo the README screenshots that no longer match the IDE --- docs/updates/2026-09-b.md | 7 +++++++ docs/updates/README.md | 3 ++- images/diagram_editor.png | Bin 56656 -> 56673 bytes images/run_output_window.png | Bin 51690 -> 45343 bytes images/ssh_client.png | Bin 21447 -> 23299 bytes images/tool_diff.png | Bin 22727 -> 32622 bytes 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index ed8e6f0a..0f793fff 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -880,3 +880,10 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: the three READMEs' Text Diff section and its screenshot `images/tool_diff.png` (same sample and size, now in colour), `architecture.md` §6 (`actually_color_dict` users), `architecture_explore.md` (diff tool, test count). - **Files**: `pybreeze/pybreeze_ui/tools_gui/diff_gui.py`, `test/test_utils/test_diff_gui.py`, `test/test_utils/test_jeditor_contract.py`, `images/tool_diff.png`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-297 · 2026-09-24 · README screenshots of the SSH tab, the run window and the diagram editor redone · #docs + +- **What**: the README screenshots date from 2026-08-03 (867acab), and 90 commits have touched the tool tabs, the run window and the diagram editor since. Checked each against a render of the current widget under dark_amber. Stale: `ssh_client.png` (no Browse… for the key file, no Interrupt), `run_output_window.png` (no Stop button, the proportional font, and the generated test's indentation was not visible) and `diagram_editor.png` (no Save As in the toolbar). `tool_diff.png` was redone with U-20260924-296. The tool tabs whose layout did not change (response inspector, header analyzer and the rest) and the Tools menu still match. +- **Change**: the three redone at their old sizes (1180 x 640, 900 x 620, 1180 x 720) with the same content: the SSH tab disconnected; a real run of a script that turns a curl command into a pytest test, through `FileRunnerProcess`; the same diagram, imported from Mermaid and fitted (67 %). Rendered at a scale of 1 (`QT_ENABLE_HIGHDPI_SCALING=0`; the machine is at 125 %) and with Qt's FreeType engine (`-platform windows:fontengine=freetype`), whose greyscale antialiasing matches the old images: ClearType's colour fringes made the grey edge labels look orange. +- **Files**: `images/ssh_client.png`, `images/run_output_window.png`, `images/diagram_editor.png`, `images/tool_diff.png` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 98021c6a..291f778b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-297 | 2026-09-24 | README screenshots of the SSH tab, the run window and the diagram editor redone | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-296 | 2026-09-24 | The diff tool shows its diff in colour | #feature #ui | [2026-09-b](2026-09-b.md) | | U-20260924-295 | 2026-09-24 | Automation keywords readable on a light theme | #fix #ui #jeditor | [2026-09-b](2026-09-b.md) | | U-20260924-294 | 2026-09-24 | The terminal font holds under the IDE's theme | #fix #ssh #ui | [2026-09-b](2026-09-b.md) | @@ -378,4 +379,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 94 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 95 | diff --git a/images/diagram_editor.png b/images/diagram_editor.png index ff24bf0c8c906f6255abde1b9bb327b3d5f05564..af2ddaec273dbdb33979eb8a344c4c4a35810ba6 100644 GIT binary patch literal 56673 zcmdSBby%C*+a{W}y9Fvh(c*sRi@Qs3+Koej;>F#qSSSRk;O-WJTd)AZ z0tDuzdww&Y%zWqkajrS%aFy!~$?7Z5de$TNeXm5kQ;~i0ko+MC1bQMbC#4Po{gnX% z-5tTc3%sNE08<1UaGd3I-9RARj@#cmNvyaOAkcG=ywqC_uZ&$d*pKk!=EvdiOB&?wb=atF9NXoMki?#~O*SWF)7?K?c5|Eps+ z^ShRUIa&~iiy~6!&k0`r>-bwy-`XdqrwCcyUa!2);es3f*X;nVyV~2g$2Y&3{$Dy~ z6qbzdc%&z@vwayh-g~pu5@5(v?XZ)KfQ@-?om_l;FB?V7PH^)ETdmaee$dYxPwVY> zFTw1Ng1}vq`3`A3&kmf%-BToHWeFCbINw|$E|>iMYup7b`M@t=uWOJu9a6Z;H4b0K zKwDZ`g7II!u9i+bQ09 zv-B1SeoYCCU;ln$r^SZ$ zm`!#Np1_sjZMwShbcDZacR_vm@(z|b`XWqPMkXfn6?0S2#3xZ$STuRU{GBL{`#Hp9 zIum4%L*pbL?9X3b2HVs{%b2C$!LAQHZmQd83YiqTDklU_XwClt<1c3 z5W70xhd*x!QV6V;PeXK086K*h`1t2Nu&iqeSlx(uqHrIB72tL}6XAD#-RkS9F95de z=71|RjN8Tgk{@p$%GWVOhelJnp^@#2Oz$mMzGvsr<>OKbmU$Yetd+l&w8yhl!K@oB z-H%ST>cOTg0GXST30#eFTH$8f&z@3?5D-#O9Io}|5tm!&_NKyRvEzhO7E5T2tBrVk z`&3^jq_Aaeg?Mg~aOixPKghKs8O(Ye;NH+j9D>ifG3d23s6aIbx$vI(^0}XmstCMCUaFJZQmNca(3!&a)otbA`bl}pbgAZhY!|Pa zs^$i}69n9Cv}M*-Cfhb*j7{hH&3x+nSem&;lfg(!WpW(aqs@L&=k1bF>-vU8s9V4O zWZBvg>XCp)J>Z!7{8gggz_LhcxV>9{GIQ%uW2bDuqNvHPwW=x}ObTjf)mT{68T)%+ zeG|OVv1|kvpRMyp?eC=vfvaMHI85=wU(Kq!bhsqg5Ls8YQYNuir8`ds#(B_fO;V6-F8A>v?KT7`TDKk zo&EXR<*lEs;uwKnTGWj210x#>H*p7!l7MY(q{+&>j;30 z9C&I;#5L8wY&}m>aAyWFrhJlyW5Qyun>_t{J`OMX)WwflfByWL{QP&x(wBFwM&kA( zDS^hAM!W1g6pcvgqxH>wN5{kJTH)!zNspNv&Kqoj#lqo_FIlH?x5mf$tzvLn#l$^M z=OaBR!aiRJuGY%QIyyR5>hbiitb3sQh=YEzE?; zKyqP|rH=tl+U04#>=cvO4o$i_g4!@uR%cb$*WSR;H5@ZMYN4?b?{o(kczrNI7Tct8 zXBRS{2!gMF4d=^-n+wUVkGxk- zUMFN!)vQz(T$aKEpWSz}tk7-Db;QU}@H?7Wp_T>+G!pRH!I?iHHk^_b6|7iMg&Ws}EKnHY`Ol;bLO&P8QBCYtl z4Si1yuXfatS#ZT6>Ya^cT1mv-z198sdHJ!FqGB#%8rs_Vb2aUM z7ud9EHbjO&yFcr?Ws60N5IQ>!&OLF^Ym_s=8MhpWiHs2=bmm}VYx(f$Dv2eT|Jh>{ zDSO1{&m!(?Ta$s+IJCQ-neb$lp(#U1yB=?Odp#y{(k@HH-)Cj5RP-_+u+h;Ws?)Sa zM}dfQV|`uF>iX~Nd+o%%?kZiy@s1J_ziTZA)0`DDI>(xPk?b(f4j|^h+qe};IK{<{ zVHL#XQ&KWA)Z#8IJZ$#xQ03+2r@%wr<-F4U_Tdu|U(1BOnM(ioxMcErNlD2$vt^y2 zhzQ*YdM@)Tvv>@FyyNsUZcmAX&?hpnl#maH@%oL(5-Lj}MH8ADsFh~fyM?7AKk)fw zpJpybH7PhAm@5yQKXX-MoM_6GK5akOY&kxa7xE}zqG(-J+z)n^j&$4T$>Oz3j&5fU zcS?Q%RlPEUx)IX?PqO0F1p~{O@qd}=I-D-KKK-ipo71q3F1RPDf4IB*av|3Qvr4V<%AfF@9P-IU6%(9DuVknuWc$R2#tDgq zN7GlE@huu&6%`$=JU7s} zw^9G=*DnvD%;GRv4flG7$e$madCA{rSHmL&{pzgRV)LcqVZ7A$v5>Q8zNn&T?MmHZ zBX=?mgqSIKxG(F4ud}nWt(YqnnV*n?&ld&mW}=UEy+87*ri3RKs*W~-5p^N>(~k3L zJa!9_-rQHHHC&u0zC>5wr$0Io@=fhzXO_%n>NiOU!ZgLw$6iK?J`TueGcG`Yx?{3i z{4Z5DV`v7`gm0GfmweBV{0_4mCP9F;PEQ8OZt6`#TmjDrq7nD>D!7sV#e-Z7{KA@& ztx{Y^HFmU#3cM2NFs;F+eeBbUwutf}x|*5hx4P z{`z(1jxpZU&dDAOlEvd<4?iQH-J`4=**Te946uV=MB^}$aA;W1j5xJZp6@mNcqEhG zpX(1*uVaEzWWkd?8($ZD@G~(nFhJBhAIipNe^W-lR};nx71)iNgp$y6NfF{PD=!=E z*7`RWw=fslH?32Zip8mJ8|4J)EW7)x2#r;}YBUDi4s#kc z{eh6WV9aGQwL$TYwe$nr_N&ds9VacO>Ab_sENW`%{r%|eRthp=%+hjx`}MUf_Gqqy zA|BqEpEt^HvN$F742AS;6h2#7XN}COw;dZ99BlBL9TWG%6qb)N1;0uiXu9&_OPR%waavVPFnM%DkXmx&`sX+jG6n?o}p6AYEUvV^ZG>yembP(;L*#hENZQCN;2a0 z_4U~*4_4Y>m760~_CGTbyKB0*;)N7`wBtRxm3r3T-{(s_8XVeFDk1d-^-h`x{;4jy1=VKVszy-HR{azeh6DtPvz3y)mjAUzknzd$TuztCLiIhZzHtfV1) zH4&-G&B0OSbo0qVA!V-Be|#%Wd>}&<`6Ks*R=LOVFyTeOlpU<7!J6T^CWYnzmXNJ>Yt|aVehn<*A!mb#xdZ*2tPsTA+H~qOUWPzw= zn0mR|)8!cBpx4lYM2ZKFu0X*-$(1_1D`B}ltw#AcZ7fjVWC!qhd$Wh918k;6cNO~f zOp42}*z=s3Ow4uwgM|mwzjc9UkmSL1|HJiZ`M`ay>*p4}X$Rw8zh*{l^qm;?)j?^` zHOsVKtdrc2OmSCYA~P93n>L;+a!nDylDb=vy0_`R(=vU47?*1>yK`Vn%w%+QI(g$R zsonLO+;CvSs=I@IM_V=XwJ^ehqlEwnsb=O&NkI3RLmRa*Sfz`h;^%MlbkIYedu8Lb zNUB8A^o|mu%ubD9)4n-W7_y>gG+&aoRP^W zmU8^_ko;C*t6GkiJd~fasksU2>qGuoh93{Ft~c?gi6wS^xt+axeW!g)L?l;d*Nj6x zoacRlp{vurU5JXaWpAr6`9t(M|shyD`BhFLGB0rH|H~8m1iv2QM{{Db<+~rZzrZN%c z7k_o`#F?dy3gQ&bQ}iAl9BTxB`?^XkjvZe)CoI!7j_?q4+vNV=Z~@rK0yZ5YH!=GDrB<%PN}_b4}Mo6!~JjPlqqCEE+#!J z3|}I@P=&5mldrjXE;9*dOo>+bO+L>Vb7pFcfQ^8WfMkpfHq2W<5^K)dT`^!1EMCZ| zaOb0vj6OJ}Rl^lw9>c>eO9BE3fW&QX*8zxJMh)xV9lJz<1!At^!GaLt`O+ey?wjpz zUSDe(>$;V3VO*0B@454a_;wAMO8`R{0!ziwDXs_^+xD5kMb)&cLdPFOxsDzMx5L=~ z8Ae4e$9SWD@wu^OtX%6em%H_IDxs9OZwn(mUtob6oK>knpwurZVMNDUNr8Td>K%n` zEKy7RJsDh(ZEnfE+k1PG4;-Jr^l24aniw^ej>4v8(X9CJ%6U_wY-X?Am~_w%)?CPP z2ztYiJS1CWiZ0+ad#v_aq5NlZ_p$``p@*!0*n?);x{B$)6M4-DsvU1^nv66JQ`C~h ztC@)v<)YfS?FH##bv_VFR9w!!6NuGlRGlB+I*vX}Lb$6Jhc-_k6`K4CE@}JbFh_l@ zwRs|nQ&Td@ovj}%P-+&3e0Fgy>SIdcR|~ba^_Z+#e}#2uvm(it3&^-83kTWpSQ4yn zKKdvkYFILj@11d7i{pyxPNe8C0|%8QlePx=t4{Jj@r$|a$I_TWu~qii7gA&Mr>LY>A|#pCwHNu^*-%XFNG-8Pw7b@@pz!UwtAGzUbMfAKT~I z=&h?V>1Y~52o6f(ykG|E=yVDh8Wn#5=+JrT zcgbi-z$f5&Wq$K%@ruXRYfq1N$oL$!;}j~>-BChZ$v{8P>fpGbEk^AaA)c$gPR}S= zHR0W?xae2WD7^DF{0OEZc+GiWERC(!{DnTaM8|+YLW6}Fht?oE@0}w;?@OMKG-L3y zG2Q8#dzpE1zSm!#_#k#-F7D8XA^G)X%FlF(<&Pcmci`&T&OTS!bo6ni!zC}%i+P;F zm-lL$F;CPGt^T)*zquqV`Mx69g*Ez&GC4rP~X!S)mdifT#GYs$i(@x~e*z zkWLo+STLnF^g@+YzPNv7&jFllU(+Ea)}XU8FWTyNbw!^^AKbIDx4NFGjNM5@#ZI4@ z(g-F&+Q&Q*)UE9#z#QAyn-{1QqmB+;PzQ1XqSvZ&@gpBROLNB&Y5aEfT&cHE#3)PHifX$dR-z(#~Nh}H77PqV9?_{RH?ZcBuREc(f;aeJb5jXGhYTc~L z4VZ4Aq)X#A+b$I!s=p~g0n;6V&%HTNeWf*c-tM+}54=CCU)h(wPrVLkEQ!#jh^~Sc%Xjp_qsBYsuJH+KW^xPVW#zg?f?KV{*IQlY@54pV`#zYg~6wS+;wSQFP z5{mo8T@fsz$S#(@=*cY1K(-L5-*|?nOW#|s(`J=!<|>+>oQE!8qh8U;F@?VIxrndw zc3=*4L?HDe@$+<*<%2z{TNI6A|6T9OD;MCY!klJ+Vue&caOS`&X*aEX3j28 z$p<)i7s|9W)e^lQkv7_n+QVWDajXz07vtS{4agDnqTMvmylg;!1|6X{MbX`eT`@6X zFjCa@_4^7-aJZ1#9y7J+_$K2*jCTQezJN+L0-3ptQdZ=;4R79UULPV?7dSa$E0-^B+%k zP#YZu{v{L-edYCctuhFRteEj%1>hdOt4mtxC?cR=;c+=S2JpFc;KXg-@Rs{gFoi@pUOdcozS3h}G4Cl-+KJI+TSfOkc^8R^HSL94DIosu|lY!)&F2vuX9i z!)}A(L21_8W@9woSi3^c4D8tXaQ$d)sMUA4jm1feAo3kiE>omRarw={O=nlyzPQ)Y zl63-0mhtwu9?kW{wmqXFU2kbl*bYk9?o1p;L**TxK&!o7HLpcSm-N%n-Lb19#6!ht zv?Y1yRQq|#`DHSYDheOU$J4VAO=q@M0C_bfQGql$*f}z<%2hrSf8~en%PHXPJk!MA zn--ZWWd_?JRpUESA*S*8bUZ7%0})&e^0`(MuItHH0tOQ$&y}~UtF@)U=PhG_@gB$< znP9&1!YE$W_KNz4w0aXh$*wbmE-QH%qChiP0)$C@U2Ix{>&u^x`z)@0F`g-@uFr;6 zJER4tDtJnB2_+I)S%`aH;{G**!?nzp-3Ra3qO*Vh_wR|thVhh6dmvAhH^%O%CmGhL zjY?o^^YfGDr{mM{G04Ob(+1P^Z+v|w9dBS@nBZHvDj#p|?nW#z)0Oy;*4?Kyy*O-d z_=B_jgpvMiArM?VDIxszkaK}?UJ{`Z#ErA zS^-`sjpuq3AQGCIn`K12$7&SMu<)qoHW++X0mOlHAhpI49ty-mhxrzV-K{vGOciV( zg3`;|UM_dP2)Jq*F21$jS`Sftmi>%qHIuPYJiW_No+;NO+TgtKr;jQy0X1bt_g{i3 zm6pHm)HhlGJ-VfSGT3+j5y=cdCIGFl&)IHr$=kCk!{kZ>hX%dj3{g@$uD$u%wl|-y z?raW$w-1&l$hb8lbR}zjB3TOQVa6(#K^~MWK+=H>O)Hx2Hf)Q@ph)801K<(r0lJfWR+Ksj0;TjAajgI0~ zfQu%7dAN~``xg@Z###?y#oI%x3WG;^jutLEpMdlWMrFC8H&FbnhtR8c z>&=}%1sNI#h<-V^Z=uP`CJV)>umwizw@ZoRrd#~Hp zjZm1qkxO3R3O}e2n-#627akh%%v2~IpH+E6yzb4|6Ui3*#a-pF=7YN%sy}7%bjFlV zR#s;FSco?GXXnduRb1F{v5ASx2I;=_sDdZw`s%7-vDTnU_VHu9Sb>x7)MWNFT+PQ2p%gmc@R0 zxE6t*=hhwDwmYqjeaIy6-DF$~@*tr|De&mH)aYiHuvWeE=)rGPTk{2yH|Z{EE!)}l zc=h{ky!P!I@1YFA0C8vQxdFl0zk?jy2wPoZsqX`j$mLWeEHcj?-i9i_&b~yW=dLwt zOi$8^RbQxPh%{I&B9T*-{tQ2!ibto+_lCkzk!;k!T#Beal{j`h@vE?mdYdd?{q-CW7B$iB-M2J4g)`tPo(R3WHxNSSqfPS!3XMz)<7l~ zt;1IuXv}9yf?1nqjQOC^V-#8Fj-1MEPKR ztl_>|Hr#COw8@Fd@-mo<5{U$*KzYYksZ51tinCx!x{ktsX#rFiJzKt0G5V)yZuU^V z*Rgap5h%b@?}sbv)eX;m56yM7FbuKHCMNaxiAwgU3H}7dB z*{S?Wh+{-_q$}#+_@Ix#3KICdUQ@W1tv^x|n^wMAs*Y%u+17B@G$K0Ub#oJ$fdSo% zUly~rtlKL}Md2g(`V`V}Cl{o+GWI~odC>$kri1?Sbu~Xbee5JHnw=<(|S&rcE2i2w{{Y`Fi>rwJ4j!a-0<`kzg0=Zfsl@?X|pOP3u z%N^My%(qm@KkJ$%SrpnLNIgaBq(vTU+6O}7^dPU3qZdXETUoQDqQ+C!P0><^S1Z3X zFQYuZ)dh+}y{GxU{rs&tQO49;OAtusNbyeCtkPpIK#+a~{V2hbeAscgq)_r6{8T=4 z%EKe}SE2>9A_~$MPrtxcigG4KYL4U3%A8JMv3b~Ny=Nr&O#hje6>F$3S0s)uQbFo*hsx z+IDr8gba;7Ivdvi_7U?G67qCYR!)A9$-81NwK(C@k+%K0`O=eZ&8%OG3YUW3L{g8Iw*hxWBpYww_ zw*G7H>{xHewUds>m$dXn{{a zll`-N)Ne~UxUChaIvm8GLi z0?eFvHkVl@<#{z*@WK||&~+s??T~?%yAV*-Z&nL+3X>ACP3@<^CdVtU)B>+*3f1K^ zs~dG{WCMtb%ukd`SriiyLxjVN_E4Zpcpu1&nNu|O7)jxvm+p-0LT&})lU6aGERK19 zk1Pi#CXi-JFVx0VDKPggYX5cr5pBm%g6ryb^9s7~N4e{}gGc>J7aaZaDtD-Q|K&7q z@LC8m>CJU#0}K#T8fVX?1l@EUH83x&%m9y3F-W{utw5}}Wt%)LI1IjD7&>Vh&7BHV zTdi&vdh8_HV18ODdv}d^%jA+`Rna;NWr5KTuK+xO@B6dQj{AR_#R#_k*Ku+CMn<## zZmENo`J;f8CqGLpy4`n>?5mj>yJbRDdHLwzAj#LCALMjaQ%%i+E$b9j))k0R=uL-_ zg3i_M;h~8zfBx^9{gzm`R4Nl(2fsoD6b;T)g{pNLAlvCl{fQh+X48&g-Q~DZom}9b zE7cc)hsQQ+2yUx7%l?Z1%o)H4CfLzV_Di1nNlP^InIGj`B4I5oNs z&eawwPWv{hEMDKE$_y2a7-FRkjcjP|-`hcGMUT;3zCSz#S}=Jn-LDgEc8@4LJW9J9Tcxj-7Eayh#0Vt#&%(s!wF<* zsT1gIESUMM&mfMisW&;r>mz&iTtmso!Vt2vzL_7v1@~A9p(&D^O2VCbd>i%wOKg$wo(Z>qC!j)i430}?FJYy4JP49aW~-efrywIk^Zeq& z-m{VVZr||T+o#FftA|9IoZ?Yy`9Y@URc7nceKGn9}bk9_w6DhNc4t= zs1a;xCD4(i`iiz33 z)UE#e{*kP-boSNi@yEA833m27Kw~jhOI%*0n)RVZN4r|zi7y?s*5`3KWJoIovDf9V(9P}X`-e3G zdr_ah6smsKuGB_aQD2d7rzsm5Lo9o5{Ut0();3UH(>2TtHoq3rG?W^p^*QsE%|T8z zUzH&l6>+Qc;TXV=J*0I-qj~M$wflz+MUbne3H?6(u<*BZzKkEz)*Tohng1eS6w$9x zoz31jaYL^1OdyBS<9+N4| zY#x+ZpW44%s;f{m?eSA=|KW*J<~beD;G-+W?v_F;E!&5`C(prcj`b!21_9a&RG7#lIHa*$g{qnD-5Qf$2tiMV#DtWdiO{gxJ32t zacUiv5aadBMK4HSrDNtC4kNXQ`-};3CcbGWwZ&0NGjaaoKtolB(p4m5tDzS8E9rH8 zqfEY|uvlu%fO5;ws=J`EP(X3{@ff@`@-!}|O4w`bn?vMpN+CwPuXF&q-bbXer&--& zZSjK$JtCMCCG0Fw7&Z{d`J;LD#QiW< z32bAe_I2(vkJ-DB``O|DnQKt7cX!X|Ef}{7efv}GCD>@h?TG)Rl@V5jLrUg zA#N&{G3OTNP|9(92+BDWw|*3upfZ5R2)p_L5TZ5q!3%<@`u=HsJw6G}Yf?Wty#b~; zrR*FpQCB5yA;#Q9-xq=aJF;XtkSd&A^oSa$`>Okamp?nLU#BMwP5PuUTVYuzLc|KW z$DcAfbU&JOQ_`Oql05lyrHoLp`0R3uevHnJChz%$9JJWhKD>yoW8;NqK&i}sVx#C8 z8Cd$b_4!D(Q!>1-qN}Pb6PQ;``vo99Ph?1GrIa)@#vY`ppbBr9pnuCiotK#i<1r%M zP((ZSRv+*0pw3CZ39p_?x*$TGU(4x=gcPT{5m)!(;A*Q}F&9-O$KIdoHIDlCT$rm9 zHf`;C57|?4V989HzV7UJsGICA6ciOTX*+l>H=e!lA)Gs1d35w?Ztlj+*McWJnH!3A zguN|I)f7TMQ)m6b^(X)EJHkdKS*wb_JzdMdIngqWYdkHN>V>wvTs#c7G|8K;(X~B* z+W~(5lY;kOO5T4RgYbXB$NRbMw6wwC)&;V_yjxn*OH-@7lZcS;WKCX4qhPKEMApBw zk62HiHr{&$eEjjaV46atrPj;K!rL9`Xs?ktw+tfdSxS9bj&C|%8)7SaD}F9etBJSR zRSC@B7B%#ClYaQQCW?XI}!8cg?Cv|Ib? z@pk7~01hyA27EaL?;W>P`pVM)Uc8nFKci^TF1I`ZG?%itmyWA*Nyd0vi~u>1J3kiU zrb@Zc?u{usb#L*Od;Z9`j z^}jE{K((>nlO+mJVU1;)I07E-$2}&V#!SIm=c8CK{%T=Br3wf?0pZH~N{+v|bN*0h z>Uq;Utck3Q!PrlpG$`9l6~vW~J|ibj?jLsQnvf59NMg|HD5;@wy!6(X*J*BMsz?IO zk4r5^K;&NS(Z50CKOzQ=9j-tmM3Hhf`@kP0^d7H<)AS`XXIk65kh!6xpa8P4;M(O= zKqv}n@-)_I=u5xXezmeOsBhM#(i8XMGBM&7+oD={?M(8x{Z>4_TMhuSfMpzF0ex_V zAPO!zAA;|DMsx1Dg(nfNLM49e45kq_83PC)puOCkt!c7ASw;n7l(B1U`XK-=0BC9f zBv%qX$7^PqS7+JF%a!w_biFou#a!O z)yE^D?ci57X$ZcHOEL_+?}?o3hQr~e4FWB`OKkse@R#;y6P>y%)M9?emp>(GX>%KF zwHlQhc@G9t0U7Y-ZKk%?(b3Rw7rSYJ=tj~7wvxhJFb6t_`|Ee#BrMfZ$9Ch0vZ?S*)E}kTs#p8brv8S zD(Fw7M}%n3_?qPYeLp5sKrnDLp1&^O1+$El@>~c^YLpZ?O-7hrh(5ys7}lc0E~)jfzSjE_B>W18So< z_fLRa5l~IzT^&We=vJ92n(;Sjsr!1z<`o)MY1r;^GIs);zV-507%cY9 zW;Ilqf?os>#R3w*1P$&@P7CX27Rt{d0O)v23t z3P}H>MC0(Ed-oA?J@#5}v@N5$Z}h^T^nVi5z*+wg)c@Z-Qp#3O17cb|fI{Wr%ms9N z4}xf^B>$fJM}#s=1)#zc2gucI^60J`ouI-fdBTAG(aI?HtV3~(&bK!K9;=_;|3k$a zjnupRo@=lzp$hT;NA*8lwfIgLU*PGV#Ikf>77!o>9PsPbnalIE0t!wyD;3cI>%~xB z)-+z5MC*O1hnC#}Kv!M{zy{e1qNIcAna;H4&u>YNij`51I-7=IwIiw z&N6>o{Kumyr+GhMl{mFge{s$mwayT97%ObAZdt5fS2bgNOgVzwv(;SP^zreDq2RW? z>gYxCS-b9Eg=EXd;!8Yx97^^+0H@`^3L`TZXxQdDcAhONDzaj13alY(U5ySKiDJY` zXG@F7=78}?;8LSRc$t&d(8@XxLGntrwy1=`VZotVRT{6a0m}g=Dy!&G3^1f7rt)9g zI*dptYHlXqdypz2@^2;28BnAVmY>7zzMwBNfYgft5Mj9=%4ZJBb zPsE3)f+^CWZ=({H5cMj=i|BB7U2C>%dS`qNYvl1CPx%q^Bpotw+1|W}d(n4#e2u|s zaz!KDg>;ic@X;u=cmO5Aq5z-`RZ`mhSdF!FaslK?)ieRfzHc#G%RlsRei_jl_OuhhvkcsGJs>W)naO>q_Rs(f`t06gC}r-`pMnu`_YzJC8}(Pp$$ z>iB4@1M7(tAIBQ4FjwQV^28UH(n@RQJ_tXQ^GRqnPs$AcM7n=ujR!sv-04C4^FGevJ+=*V*&ay8 zF(#5(;O4CFEAoMZK|@7gY^;;f4L_pdNqO?g&cY!gA>3Wp zaA|fN#$Q<{y3l50_=ASgfmN@;{Vh3Ni`!1uw@=yG;=Vp7PEj3E#QFFu*Kpy$Ea7zb zPpBg0d8k?c&sONz%NHO}M)cb~z;-cO%Y$XDmv};MN9WZjE8I)(TKpB$4{t^Z3RkQK zv#-m38edv({YowqQ2WYUe+ZJ=POPqD?nL^P0R6DMk=q(?&^|kWCh#T z*UW)GB_zW8N$*W_8ta|xk&ky_ysmd?4r6a?)Xg5<9OpQ0#@Jq9!-KApl6$k1&Ufnw zGpw^tEVR+@BK@h#`ET3&<(+iF-+k6y0V{Gocwnzrm)8g59m)_%W}5r@bqh$SAt513 z7dGefrG!M%=vXyCIT{QEnxzJ}J>8ZGf0JTYB#XEBM%Cdh2a{qX!`48*SPP0 zK1wQ|p?*Z0mrtk7kt*)DnP z-~f8)KF#WDf97;Rb$}QRxhoL6Je{$d~T>3BhW`3zy=US6gG9KNy!y*ylXtV#V%( zel*Fk3waVs*y};w?a6_j*WtAIS&VE&v|ou#0i^MsPJF|~bjZv87844~)VtDZSyI7P zgB$Gf>c6^~c@zai85s1c$0-aAZ+ySJemxq=25f40Y5%;xwwK5b%0A-w4AAjK7_HDV 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

%**nx=Y%nVB%1?Ti2H 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 HYLQ7!E;$(kY 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 diff --git a/images/run_output_window.png b/images/run_output_window.png index 18fed49ab564500ad50d38b638970f1f9da2a476..a3123934e9e2d3f54467a367a612bcc31f45ba7c 100644 GIT binary patch literal 45343 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 CzQbAo literal 51690 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�($rb3X@0WGC)h?9_^&AA! zs}XT(7J^!g)HOQM!Dbotw&@?Jh9UR!Ngi6BzWKAX&dtk1BVAit6Z5_GP(r34m&pFk zkHm;&XL9m54QDlrl@-7Dt(mYK(9w^!n-c`%uNOo|Z`;|my!cFRMj|3R98MI1uwl>1 zeJCxyxzVel8!!Pm4Shh(&Js2o{Y(hiZ*rY_;ofYGrlSBLz!EAKk)@0bT&sE)@ ze9qrp6@yUQwD!+?Xrx>D`B6RTLmpll6WIZW3iAFjX>lJv2i!q@Z2?4Va`f84;BJOS zg~^nrr|(^A`lJf<%jcihVa>a`{!y%JCN6HWRQ2W4b=UXMF@h+W4@wYWUsm|ml^6U( z8UKo`aR1fSiLMd)2X0f60!Jtm)U-9ctfHiZZ_b*VyWw3Fq;H|)&yg7)@79vR$kM7S z9}rm3A{0p8)VR1)?|&5V5$vMfL7Hc(YslIwcLLTy@rdL9;mlc?mQcpBbN)wupYKrC z-eW=r*K7IQdWJ(_FSQ|C>qF>RT6SfJ5H}-Vl>!pjPkmi6!R9?BXa}6@d~vocT*d zQg6-N;^Kbo+3dEhEhyF{gLtUfw@FD|8z$*rsPnYe%|IOvab%L;UI7rLx;uCb=X_zc zz39~#t87d)0@B5Lx!>=@=N}U@nS;iUjg8ZG65Zk#+v6mvH&;N~?qF+ANtpmcHFnMD z7dDmyvG7WCx21iZ!u<-Om3izQ=*?7>mck9ZDs-LzAL5?#!sj+SPNj{SjrnI!<}v*~ zSJpD<2$x{tA+|$Eg=hG1MK%`=KfhSUw0Edw+g2~LXEVQ#S{Zyq&)*kWjq|okQ(+0& z%Y0bLmE!*3|1l~tkqS9IPAO9!!OeeY{Vpp=L`lgmMaG2Revp!|SNS{!L7_zn>=b|l z)}jka(o;I|4MU3J%&3BWM|IL&4@>CX^vca;3U3F|f!F0{=gQ}^gIXl=jMij$GxK5- zbnj3maI{3=*)R6fin+sg)DA)IYU8NiKX~)Az;`y|J_9|y%oktwHG0w=4vGc3Q8Qze zfs+!P%*q+*T(j4&6N_ksS=qKn#7AfOpn#b(E%>||Dfk7irDw$C%FiZ(I7pbgtIizIMfKtr z^pK~fx|@J2V7KU`Y!6*V@u|7#*1M;0Tm-?3^dhP0aQR2&R(<)3vKOnG2a8ibR&8C< zDt1}y^3Gz=1p;gK?C5MrzEK(eRhU;Doi5?$-V@w%x!~=KwA5NFgWR$6RU96r?WLi$ zb@ThnpZ3R1Li4)*M6MxEI*AJh(h_D+LODzM&9w9zWX^PeMQN&1!KlKYt==J5EgOK4 zdLBb2|6wWZ>C{6sKo)wq`;Tb){>FLQrT6J6mHw781ehpJkTg<|^W(;$O{MO=7lNQs>{w7f&FJ=~&!WUbX zTL-xzJ+L3b4QckRUkF5Vi}u7EOYVY_42=>ulUt0MJ7J%x&YnGpod}>IeKJxp{jGY~ zd1EIlTs;GcuFh{6#?aDp7WosGi=|4Aa!O@RJ`~J2LS5{aGE(0HXCFUSfM??fNHD8@ zJDzaok<(*{4a2)P+hZKI&2T76=q*&Hitj&pV)H(!CM|KsNqw04mX3$%Z`&x(=u%!@ zwxcBz9sQ4?ALeGW0#S=B%lbf6R&%`8rw9j6Orz|YvCUlPtmTjT$Vg_2sZZ1NsvG?U zf$5cV?uikZrKS6AWbWD>crb{%$#i>~ql)oJ`UTX-%Cm;amf6dge+iIa# zRufI=ZFKb4&#O33Qum+jX2*5JJF}a8w;899hKYUHS;BIh^TB00WG>vx)9#p}BDGJS zVhj}-`a<>IudGaaeZIG?s;>_-wE{oixoQgFWwq;V!qzWjqxUiyZ*pv5?4cm4-I(<^V=O+& z!iB0P2flwCiB?FIk(O-RDkv&aP>`3|?d9wGE_q+z{RLp_?(AIIWd(-U$a8INOKB$C zb?G`GA=yKt{@i7-rdH4mX_@{Fa)&M4+4ALgB?mRB*1@+Xe@f|<-m-@6mq(EKZ=#!x zX+eJ)R=C4%Itow9EivHqOa{8?0l@TBu0HW4rS}TU*;wazSx>k@kAS zf1A(iLnD1!1ksa7GDdM}pJ!Sdtp4@4pgm#P2&U5eS(gBWmdu>&*SZmy0K$PwYis4s zhk$p>wlUh-A=t`&HxfO+KlHdj2p29^^4s>iZtQw;3a=Xb_d)c-OuECbfvgD*6$8TS zC~EU3&Ntm`10$@j=CK14TEZ85i-3m58ym9UTu4fe4h{{KLtUxu<&lZD3Hq0XR(B#EQzyGKLU>zL=4 zoWpJrT#}jxKJM8wzr>A-7u27zG<9^4Yh5(21O(Xm5UZNKnNdyR!j46*LeKPi^B|?t ziYSWf&sLT46z4=oM@MAHn3ya@k)5{m+Mxo3&`qf00swevk&%x&qx_s2opv6vB-gv3 z`t5d9W_HsMM!Tw}VR8k@RAvh%<3@=VUb8cE0RaKfRw|TbD3?}$tk-HbH|Lm{er}{g ztv(2hB%zvrS07aJzv7ZNFUeMv{p_3Pa&6}0xy|+qOZktKvu*v{6P{QIu~@K&T$ZAt zqH1m3(J(g9G}<4V1WFXe6vDh;87-K^)6moFOY5yp-gb3$btCLuj*EZ$UTvVMgn+ley6QL$il(gy7>T5k&y;>KDvS}?1e2M`7k{#O>Ks!0%i{t zUFdvotq zU%xn9R90MUziDbH5~md=E9E9FC6SSoq%@ZOHkyiiz;AsLCqMl3zu&^QdOmW6Zy0Hs%T~-jr|1{{SfqVvD~S4NsU_$8wd31pGwk_)6(Lo z3{DCqpBpCYB$R>8N{%5|=D$N>B{b1w72#4&Z>0h0I%!^n9*T#_!c^=)3G)BvyS_x= z%fZnYIxJNX;+ru7u=yDu!jU{D73(b*HzAg!s9Sg34xebE!Em8W=0rD>8)Fq!ie^G0GVeE5(t}&e&g!K*Vy2&J9mv#E7Gs68kF%q_|V#k z9O-=B%uKNoWyOb8^lN?kOn8rq@8OuB>C{)qV*)gqmzfGoO2zswo)dboEG{BfKix6* zFCdrQqK@?FEL=OW`@_jOL!P9{YASWFSWJtQ4N((}xvaaS1g==)ocrHO@*0TC0x8-| zF}wQQDDsSoGV9Ly>`=epiSb2oSq0&$*}c=wySufW6a1ikUsyCE zGj-F|jt%3#siham5PsRKCR$^c42jYY4M&}*kZeUzve8OP@-od_2%+N?lGbWd`HfZ{ z>FMdeP1(@0N++C^Ie#vUk+FcsW;D8|yiAh9zeaO+Zx^MdFkoI=`-o+1OegfVdeoBS z@wJc3)6ERj^sB#$Z?tUxs!1+xA7U*>CPr3fB>DA^p{|`{Q0x=HyZ)EgQu!VI1(W#l z3EZle%l8j?*>~2iX=~@ZxfKJ>_qwz+BLV??`zTmS0?Xx!&Fp$6OxI&T;o&Di| zn{%tRtNS54DfQQ`T@&-oZu#E$;pcg5$86_K@OOnjB6%Me2xFa(Wo37n*;P7URki(C z-#ysdUf)`qX)ELQs=4i2$2b9-6hUu&?#!zlqu^W82ct!p#GYqB{8Y3ahDWFRqquq! zR3_ZkOLm9*ZxWeL(jVVws2%ze#b8W9Plg&ZpxPZe9Jxy<9_Ew)A(O*d`Df4CAXXcR z_Ij|-ohT`QU!qRZ_w>A^U+>}i9yU>dK^ma4R=GLIuS~ySxj*oGob;|Tq??_mtMvs& zR@3GqNBKxRccqbnVImY3D4@pV5X{F({st?%-Iz0^2@=g@mM+xx1R}CRQJJODucc%` zUG0j+8L#@kEinrVi=JNL&OTIL(=DNF%&ee!=TT<`Q>AqlIkLdnT}Nk2$<%Ol2I^GQ zfhq$fb!w;r8`OT3+CpR);$}oq42X@y!q|z!gGK=L&R*Hoz__TxGzm-OQ@o;;RbE58 zcFf}9@~}q`SYQEAtVnjub5Wtph3ne{qXqe(0u4SD`~7@ZtmH! zP;|nd|FHNEs`==6L)Dj!4`Oa~wY70NaJ|@~O{hT+i^GzhA{ZG_RK=k14~hs}M>rv| zgpPYU73T{pVpnwU-2e=kj!1g1y_^ztGh*f6G2M6v(kGz@A|mGwTI?*!eLV~PocI^* zd%MJOC>>{9|03?cQ$eE#0mEHRoC> zq7)8he_w87{YPhU7URZCPoi}w$8_Bd=4AyXes?tV? z{6(tZB1!j?zLEe%%8Z||{_HGVbKgI1ZLSYx#Kv|`Bu-7({VGCm!MaUJ38?9}cVU^x z0;`lk#iyS`*gu>~gVByfq_<2s&&L~azYX;Anvre<^E16!&p~(##r<_fVVkA?aR?Oo zpFUMnVdKT_ioG(q#c$mhR>-lMGWQ>?gDHAEq`{XA?zvvFV3+kufJD6NqTKr-8R_5rMKY^4nlzZoO>N zRYIJU*jSbqktn>BuCZxG6BzpaiF#0Q!dP_`Y`s*FV|?e;k<>N9s!4NlQD^~vlRl~i zyjeUUNvhJ|67e{~1Pc=vg>}lF^>weV2LW7s8&r}ZTSL2Efz`T+NlByYhM+&`!=0R5 z%xTpY|L5Dafh`71{;YPm13C_D*I&RX+8>tV>v#;7mbtSiOw<(Z+-oiwM?-TC06!5^ zIvtoqo3kAuRc(6aogk^O1b%(t=6C=9>J-yEMo$1)?>nux9Qi5`N6yclF!0w&%7{zC z&Si{qL|3^USrA+#RV%RuL$u_ygO+lQLfm~op{MRgIkn2pbmb%@dm3c-ikk?b1vZ3n zk3f}iXH91Z)W^!f%EVlBus|JUV-F&Z0E;O_9DxHadDlBRbs}o|f83q-o6hy?IOY{(TGV8z#)DQIWstPR_EnQ_(Y07v<+$N3HKe zlLCtzD#8qy3&Iddsvv3Nzx1=k>D2G}RgqZ!h-yF5MFO1Hm=8ixa}$okS}&e;wj5!( z+;c1|D@P_bK(o(od>D49mH-%KfzjZ5EWEv~A?_0#mVm3)ZdEQ5N;EDm<(%?}f=atp zq?I91iAjm6moIav0rDFv+f*-4g5?U#h=JzX#H_sRbD)53r@NyYz5$wY}Q@{H#9ioBE4?Q0~9ax=-rt2mleW9k@}QopcMJ?wFam+jjYDJ z$jlcW^Pajb;rtF;rOpAcpx$2BYA!w6uY*Y;(<#{-jDu5x(k5nRW?@@D!RM&-_^p{$ z{}aVW2!Gn!Px;+$R)%wUC2%EHf}Z`_8zdO_mFf!%3Pv4$5kAfpf5XDNlo)pz-*-l{ zcl_)YqK|gFo@kICnf0{W>*>#iCqb{` zd95%BmE1b*Bs~>rY3Yn7GEt9uJ>O*e`T0#dM|20H#s>OnabrG!WK=b4&}w?A?704q zOAF!A9|HstlZXp+rbEgWCVg|m%JOhD0Ts7~4fwZHt;k@zAoYHkV|B=>w~;5?w+8_Q zRczCej^B|WPKt$kUoJzt?Mr>I30uj0Kiv{BvKRBk<_PtNO1uXIx}qax4tNfG%`ZZp z$-PCj9j6xG1}x#z?M+8ChjB|#9#3QE;>q0=-DPE6yL74D0_i~K=Dl)LcqO+N} z$G`qx0YHO>$%lU*7g#93hMJlrJD8UwNKa0N3|{QXP{F~!X0`k9Ew1MAvAJ)1_q;M0 z%oVwG44g*}`L*-Lpbm9vGwQD^BglAMDl@rg=F*p`mo@(p_BBKT_0O@VYGx|#1vQen z?>C3?K6&(rYF-ufpL_@^(WAJ6a3c4rizV0T=Z{&2ax^&nZ*b%xlV2d>R$kE2{L^I3 z*|fHGgABq4n-M>1nyLUkoOrPa)0;68QV7bKMxB^&bHT8%ruGP{;5-4VBtB1F?HeRTg-XSNq z0+cnJ$U)4Tkl~?A?fC3AAxZetuwr>x!B-mc>mGQhBKXvyuzlbxY`(fHqof2zZ9AJc zVe|r240D$5US9$Mr#`Py9-Z3Q-TElO!DOI({fkQ|BtJK_r zMLg@xHJ8Q>kcNR+aGbxs^MkU`12V$MPgS3L6J{~3;x#I)n*hhz zy|uEZRdT$hk8Z~`Y>b^~Q-Ihr9w^pc#qJ5{WDya0lRHt(H59x+;`dW94~BiTl9?L2e`Bb z;QrRz7!5OOHU1ASgf#hhc_Tq13&C1x8nsYcef||LiOY@x;l{tAmw0lt=3g8fd?J+F z*{}a`I-ZT5le0#DOI1l1b$M1q-itDV-D{^xMF#E})GGpeuTMfD3G?4#5kOt${!NX^ zq4hKtMzGK*Euz{NfxX+_ngL(_1ZCeOu-k9(a+{?liTlfzu+kQ;&H4YW(ODt8D zZUo$C3z0&D(Xx=ydkw7uh`szYVC-5zEmmMMa21c3VR$r9qvyWJ;LP?W36ZZD+U~f? zpfCRsN_lypU*jT&d(Dp7v3xyxaL^q)XRa@%_EXc%3k3KuEU`n(bIWn}l6#C9C51sn z%jpa5__6tr_i)6{_*@k#w(1xv)?SC9f(Sl`e7lBa==(P^kfy!4c7E$;Je&CJm|6#F z3-Uf4^VPftG5q#2vH=ANwNoQQO|)qrV@lgbfCzFN9yd z#PwX$#BS-UNQRRGfV@D5%h06%(*SUU)@TxP+jDU5z1n22>Nf2ywQkA~PECXP6mxS> zPBivjOhKp=d^djcrl{z*P?1M?Y(~^*p@+O`U=f{#T69XvfHDng`L5vmTQe&EM^Vg0 z2yAKR>=is0h6+RrMmfl;gVLg+4p%#{g`#7g7pkULj{-{jk;@7ij*Qe)9?%=yTiIyD z@0j)V`6VcQ`gHxfQU12`2=MDLA{G~C+SnvH8~!UfL!5bR536xkA1~&=qdr};mH&-0 zF|&6Z$(~r#65sFF70t`bZI0kH)-m|1nPppQz!$-Qd+8!6paI9C z1+Qq>^Zcp0?3Z{dFp-2{Dlc6s{7hCB0`TXy7_e6GuaX2qOwssj?1MTtylOste*7Dl ze2g3!!q(7nY zqn}mh-{=sLQkOjzMK=CXc#_{QBM%GMx$Rg27 zPv6k^X~EQZASzG%SSNFdur^?rAvR(4+zR9nbKe^QOVfs+uw$77zCoP84%eD*40j-e%@zuB;D5 zgdLFVeL3snX0IvNo_#H4hE#RQ0VeK5r%)p*FBK`hHNoo_HTfD@AhH~w%M=8%kmt2B zzG3%fn?7C&a9Uo0wQFOJ4%?hkV287xu4!b@^2x?4==xwF&W=JVW5=MwTa6 z$0k!iqez^GQly+(dDZ`r@$PLbAeHxzOwcES()^?Thmvdn1;vIaG&%Bu3GP?2M2`z* z5^as@<#zy3n9BIJx3^=2Vp$K~wDlDcOm+`ex)xgT6fo_c+Jo@QA^j+aLedg?45!}dNAEq<2S?3A>+chaEySqvtj^2P z+!%}d;YVh4=E~9hX^MZIP*TvhQb^wb)M%zOlom-TlHsmK?g}Hkuc8wHss)*nS_S@9 zOJwQ}6`gS!#(cM9F(K*}o)SX)A92v#4-ff_Yho{s>$aE&b5#j;d!XKwEAcNHD2F*l z?)kr;O2?`G!Hx~%>Q8A)+0v*F{ju);E9>(wSa+yIx98n#M=TafPj{*xPoJkUx68@F z_G#Vvbj$LAAJv-|s6Vu-gLG`>=PbG82`Lmwi0WUokM=u?EOck9QH$c`me9i*s)~3~ zbwNR{W9BHb47#Y`pdf7%gXE-oKWPrs=jq|aD!c=J#a?ZN>+YD_zgnvekmW-ZHx}&` zr#!X>%teQ`4sM6G)*u5t^}qigY9nATe;yaO$kNhMkSyANbD};u8SDE5H8X$^fX4^D z#zj9N+T&x3#jbLJ3@85q>2a%-%Pm)|dmL&a`To}Z{;N`hrNb=mFT=>}9^)4g7?av~ z*!l`o#8#G{SN+;H?Kin_EH5o{p!!WcMu=`eRqU1VJccnq%et`WO*NcBpI2`jkg+nm!DsvS9mz6xyqB&Xs}U$(Z+KaVHmNvvzH4 zm?css0u*F?%UrSkAM*%%+pH|N*v&lWnF_SiB`mG1ZU)25Sg#MkeE!-681;ada!0!E zc-9OUR*XD+JLr*7W)TwxdY{X}=v=;#xW2n#g5!MPrYBA0n=oSYa!2E98Gp6Q-MdsP zh6a2!caQTdXRPwO|H@CWG=Hl_1=}$PUok-MAdW+|?65U6+?wlt_6!7Ss4J_|-}q)1 z_;W=4hHZO4^Z6-YeXq`Xb6GFwJu{(De}^sf$C09n3JvmstgP7v)gBDi%VbNUZm`DX zB=BPU&z$vDq0X)@7_No+@fkhgiLsH5#Wbi*3=9mgDHY`9VY%S4 zdaikZH9-Hc$=kfH4j7c-b*4ooLRU85lODvj$sKXlnN9;$CZBrWd{*-IZz-~J`vc_@ zQ1fBd38+aIq9HGl6B~Pf%oQftpz4F_*J^34lz<@r7}}iVN)VZawvle^?EkWAw2x7T zB9Y@`PX4v4bBucQ`SWK5C8Z+s8gtKRNvU^)gu37oH|r9wmfKncq9#b~&&7O;oymm8 zTu){ZBvh}<;%e)1x3{*wBy#xIKv#vVZoepf2lR??(E(W{E+Ro4Sc4-e9KWgYyY)!< zb8sDjnFf%7D=p}L&+e6O z?SYKxs?eOw12i+GVxyA1Wv@`YL8-#Pe`be4^b11i*$a#4$3w$Ag2JeSM_&1#QCo%1 zu^Sprn~&FsZ9x46gNypBcU<3xre0n}F2mISR!pr^qib$c2pizPNMfE1K`1z_RgF5g zEv8|90pWPuoks-iV_^urUA+b79ele%vcPwisq?}kr=+wiCf#LIS*a*4eqUXESU>{- zruR4$QoF5LKPPSz9`o<3s0T@+bTmiFAQ)UkJZ>r zc-fxv*25Mk2ow4&WIa$(8kFxRgSC}`@j-#JaY&Zq4Srs7{2l)QJ+iX2Y!1H?%G;p7 zDx~W{crp|VqbP84S|s=e4MnIU*Li}3{-5G(Csw{|mxY}1m9xLDy0l_rS_nusZ(Me> z-8;ygU=~aOdG`~bGMLA>lCsv>KW>~ucfu)6YdWdvOU``6eXC)G_4VV zPd~L|^dR8|`QO!=b9>SpqPR|5AH=9m7cN`Efb={4j;Y3Q@?k#Py3Eep+_QYcn+bpn zwYCaWzZ(crulw?{e|5yS+vWr2 z{=kO0zaXVPc9UW7IpT9(3rH-3=aXZ{vmyFT*vF4B9)&~9WIyN%%X38WXdj^4w!bV* zItW9DD-ei1X;o!=B*2}Fj9HsEWzL=NA|8HZf;wRhCVl?O`~T^QJrP`{OSiz-OT;VC zyl9Xj0T|K03cHYndaXl*gAEbhza#&l1u^4Q-r18qXClNex^Md}vWi zKERxl+DQ2Ee*K_D!#SJ5b0V_WPm3^K>KJ^g%G5`A-Wk!j&AsZ<@Y2&lM5DzWQ5j^> zvKv}OXm7K8Ip&4YWb07R_Wmw9xu@NB80j>2OW=cOp|@iZ_cOGOjh&z<92`_#EMPZ{ z@9%vo!S;{{9)p+dmyvxNLT_KcK9g5mne|b4&9vU@4Bk<(o!3%Sx>|85x8;h8v+zT) zs_a|51o0JB*+0mI(7Z+cKZTb&vGu3W3Wd5oS}&U;lwfq@#xf?{fxlOl+^s9(V`{h=KP*#R+f;$9IMEpp)g@+zx7hT z)A0T|0`GX+68V8GVyCsf7Jj;{{QN_!ajwOZVEgSkYEA`b=W_8pL{5oNR9!>UN2Ang zBpf8%*zHQp=A6>R#MC>Y`Q`qo@hw7bgmUrIs7HFl$OSKI!=AJdcANaHtSqc(0W}iW zR$k;C9@~{ak6yWOauyxw59maWyuW@$n(kKU7BL4^A157MS$>W8wiu77yC0XW{&E#0dnGDzDy|ELLMH$Xwf~$nWGtV`vT@#$US}Y z)ommgXE?J>He|(!6>)GIvIHk}l<(@T#jeD-_vagj`1#3dQhj{LI`59(6wK=6%Jk_R zgSfb~r>{2w-$L0%e+rEX=d#;kPAj9e#-X9N$-Y*EuI-9NIDQHaR#c2}w#Hb9mkL`$ zJ3fi$??Sn4=a;V*#0O(V*Q5+OrL$ygIcd+l|6+Q1!@mR=g_o4%+6j4BUnkY_4ow`{ z$}WQ%vS6jQ#{3D~EfRj=BYjFV(((gmw?T4vsCcdwGNEe0J}lHY7s<`9C>TZS`P+N_ zna`@EZgyi3GTCm_T|NJ~8MgPTjt%YkN>GRr8a-gTe}k2YsTgroj&+-cX8xko)l#jj zqe4a@v^Y8ZLOsEbcqvbIJ|S@;Lav3&eZilxY)C9+Y(`9E;tk*&r^B9oO)PZU%f}jP z9x9QsPv76)_Zen5TowC19-si1L;H&$c!gVUZv?aIE`5Dzr}*iUOe~=OFQ-FDzs_`} z^u$YsZ8a2c_WL@SZb#&oI{wXD?t{N2@q!&v%eCN%7^&-X z8h1icl@HiBS6r87C&tNKGqbX!#ia=**zfJved(Q%$En>zCmnkpQ@m%5Vs|ob_MrAv znux_y&Zix|QL44d-Vtwbftbm*wVb!aGcZ;zNji*YX{Pt#lgyy+Mm68NxNVl}8ye8j z`9B4HgpDWA&l*8(UAg|P)%4F~*%;B4_Q=eY=X_x$V6&MdvaX}O$->Gylxebv+`OsA zZF^X(Js*8;9kyuNBBd0o#uR5 zR`4}l2zU7S_}$%##W$Cf&&|OGIv(GqnnDg%4hooco6Htl+iHf!8K=93lNg+Q)#c{9 zrTh7JV--`#q(gTOeEDHhB7Pls2u@KowLaUBRCN>-d<6^o?#JYHudrOx%PTNQl$3Ptc9HP&le%U_HyT)&n=jn#pC3N+xOi1-f?B>q z*-2v-hv4p2(Gu%f)&1Uu)CEvcZq6*FDBK+y8dB!u2;~iYDt;4={StxC^=@i<+83X_ z+&#ne|JwV`s3x>+T|E}q7DN$H>WPXV9YGLjTTqa0p;r+Br39rzfT)0o5ZEGBAc#os zod6+<6zS4y0@6Dooe=USy3aZLoPF*X@4b8PpF8dt{9t5+gsiMJ*EhfKn{(d3wsDtG zB8nuQkbm&!u!8Wtj9MFfshy$Af0$GxdM!b?rM0!Bpoq_=H)}xNv)oyK$?=gsh6Qup zX{fR>GRLUWA*ni#H{81Wm?(UG={7i6*VpIKD<9wgV)ah5c>}psXkUqcgh>0o2LH<* zs>RsgM?-W->7_1TdI1x4^ob7pfr{xK0hkKWw{`BIklB_*$L%{E&(H>zPjK)DSjP)e zS$_0_i|MPbqARRX_s#CzYj60N(Ap@p>gD`oun3+rZ2G)XT_`ZYhlY}GISze_;RVLB zVVmrshHi@QqA{#8V}&}^$mldY_@N^}_(yy?btb4KUczeF(4OV)8F-SGz;ibV%DSlK z^MyY5)tRn4DTzmfcTMT(hZKxXQ^<>iPa*7!{YeFTCt&CQ%6RW>PY{VZ84uBr)p9o< z)nYt&C~@Z5UtU{x9TDBFZB0M4&#%pK@`O|=uCKrUl;v*euhuW{B!4TcyqMKZ&c!G3tOk{+FT7?~9 zYrRbuhOj9jGCjP&6#uAu&yU9riCjBHv7QN2Pztsk%Io;sgVl9Uy(!Hl2cveEMmzYa z$&U)%FvVT9^ocEPB0T!rQL~fbX)y31kg3VOBY8FX=O*WUYB;*{dD28b zrH`8vJvBF2c~NEG%~g8Ec$bgRfiZ;jtSh4h?=8|w4gpSHD|!cqrtWwluIcDR(4P_K zy~rhb0e&Peh|GQ4OMPq}6ckiQ@jt@h1kl6Jaf!UX;61v!x9qXWgFc7x?-omWx8UdS zz6pJO{fCq7!FRTYUab4W31*C^OO>DLD%g13CJ!VyuY}w9mIZOky}+ukudl(FcF$>g z#aj=JjEx3~#Agu)Sx*NHl-M6eEDYi^ydHE$Jq9EZ+g}_nwwd_UzH)Qn3qtGjS>Hya8@vk(YV&$YBczdgS_ zMO3A(J#R^g_jEcxQJ83+p@^C5$u^0OqAx*By1?)>yLjmC!g+-rF2xe}4v#t=y8c9O zf}8gemDF=^r3WMT+5YavE1$f!l=H#sLN;m$ z-?2FZm8AS?Z3+9F;A2HEU8oPsZOFw1k3||I4*J;=x4UEP#gKzMT$0LZTGmprvZoW+ z3!cT$bFy|3O9sZ28PLSi>`X=(nQ^?R)3@T^*jX?WGhH@!k~PS%;1n7BaOPyzj6FwD8wk&f>)qeh*`ZO-ig0f{;Y}?q_7smfzhm@{>or*U+eEe~nLXpXttu35~t4Pw>{K@NKYa zI^e@Ex#=q&;p8}doWp&jH}&{p|XP}^aI6q>+{{(DAxTS9BUrw zD$eTbRV-{j zg_T1mxEDb4VM}g}odLP;oevESSy@?UGkUV)WKhV5UqXmp_jKrPQ zpzQ~R1J0Y@87ag!u*XvD(Zn<^oLq8pGHZ91G4w~Wp=mWOu{AEdTL~rz-tdpAK6LD| z?P-CFkRY&%slPE9AE6Zc4?jxZ~tqa?{y ze(%@j2cl!55A2)@i;KfM_AEg6U8hH`z)RiCILu}!8I`65d0GgYsE%vrOpy&0hn5?u z4gdfKtlC0XTR}wHbj6bc>x)*3*OhV%ORl2Kb!kQZ1xN8qCa0t*P4@*dr)^T9N}adz z5KI8IK7amcpwC57(Gd2KjnR;>{9^h1BQh~>wUIRLjRm=>h697!waqc1=7NV!LPKN2 z!lLLa#CV*jJCF=U_lTpv4PRZ-btmD;CG_u#-RG;Sst}8n%uL?44U_RTvB9x&uEj+~ z&iY-lbn`9%*QOVBMmj?WIg1P^wT9k?3dNHX6MnWsW=N!^om~=f`EAliC@NJ7e$K^B z6ppcGK6a|lT|elBAC z6*^f4^8+Or`US~KG_H2180B|S%k|}nEr~-P3hEx272Y`c3j#(QLY*1}O%v4FG>b+i zaq;edlj*8(dM{h1Cs{zFt&CX-{XoB@ z0vj@1ig~wQf%I|KTvfHP9hE(Ku2bV2uFFHCB_*N4;bA->)Z!$kktPj0Ci&Ma9qOVD zCP03x-sDwE!Pks-g?w}kdUV=!(VQ-u;DEU1;qe^!qOm8TDf&R3C45rySfBekI`2H5 zLlC5FrWDIaaVsUIYdbjYX}zN0$@btel!Eox4^FjbDJ|7{tnxcJI4Xv$>hhdaI0&Sb zQ(j=XRlq*heu)a_+16(E3cDdDZ)R`4=!h&2wWZ1y-o}jkx*_W&LH4(X%lYPXDnTF?-US|0021 zrdw`J%?QBX$h~*I^wZPR&_buSwzfv|T>Ym&s^+UP9f%TTvyS4(~)PP|LQkjm&{mm~32pOW3*&KjN&XL#s=!=@0 zp^kLt!KFQ^Wk)E;EtoTdsHS;+J}Nr;l0Aw4E{Tm#weg^fON;~(+Qw`1hHq{j>$EOf z=;KiZh+-glrOV&A`J$N2gmAkAib~bPZK*A`=KJ430AL^EE&LnRIcU zo@(mq7D9_Haph2J$S*hE9e*=2Ty?`t0Wy-`pLoX!!NqroJ7o4>(sB7K_iH)sfyA9R6l>jLQvuJ#ZIpRxnG1uIi>uWN>}@tN!Vky|`L%gf6^ z;&Dj33B$iil0jjmImF*mGnj>Tbh^LR)b%VorPAs*@Y&3&NTazLc}Rpox=;?6y8D?b zt&-<|+?5;&QFUw;sj1M58l^_bc&z%Mn?P*LpoW7>%*H}^wHOabe?VACZi{j!)QszT5v#+md>%uRW z-WgEYd8@&|@LGqC&dbC*Z83vN8N3AW72&h}9CJz^$FMi`1Z`rf+K?xi600u%uZ zjCXP`P5SPYreH|}E0E2lmhrm9U|VIUsv=E zaYIKHTXmdQ_tr1q6+tWv@8J72mpg+0DgQyIZUypr-SD*4T4D?*;YZk6B#>dDvG28b zixT5xPk){7%F9$a=Pect{h#_D?br@J^+Ew9B4C4;fAOz522S-3Z}^3J7aIgYcyIu0hecAFTtJs6C2E+s9M-YhfMvp_QP2+N>2wQ#|Sm5C_ zixs_JOYaR#2p(cqjDGg?t=Y-fIQ;=JCPv!XAv^Fq{`i=bk5;W zHBvVg-1Rf~5-gkF@Oke{e{K^#mZMQH*>pHcA_Y>qF^_N+3*asl{6m~(M#{bFS`Hqa zYcMtdH2?t72=$C+Tm-Dx2}qJtMMeuv2m1TPati`q1{W6>SGq2mAd%U{l!<07R#uyO zMSKck&mK3H(`SOxyJjRv-v?OoWh5ko?vv>p9Yt+yGX5rq_(ZD|@jP1kEMg90(=b>p z$AqQjQ@Mi{R#uCHMdY=4DfU5P;aRG%j{o?*AKHzHU&9 zTmB2+lEin3i-Y)qLgmpP%Mw|R8eee7eX@p~z|YISJv})N*EP_dgm{rVX<91TJJ2{95`K;tpU-w^Ky z$w*Ic+k2^EEgRVwA>h>W;id1j;0qc9R~`1(<5WuBMZ4PbE5pOPAn_Ap!4xmShd;_H z&U5S6AMo~j?rLPwn1gH1piIu^8DIKcgsZCBQ}a<^Kz^sfQ{Mp}5fPD=_OMJ*fTD`> z9au4dcx%d1xG_=_*dM~!+}bLUwzjC`2UK{)pizCTLkWZ-X}cp!kKaNTNd8K;R|;7T zkzvrqdlZ#M@gT0@J(whMl8JR&d?#Nbd??-qWxX}T0*$!-Bvt@_IYXswNroXK=CYkK z`t`@p9p-s_SAqN-fJx=!<>V|(mVCk&#k^V!3wT~aV{bU9)%UG(-j-fgUNCvQ%jo*b zC%C1W6+si*tSl-~S(iD|rYA==TS7*L2>#IC-qPG}L$j~8B9rlFO)HXeb913DmXrCC zJ>#M4!&Og!CekEN7!vsb%>lEpOIy9Wy05%o@&p$vS0jXAO;O+uwf{uvR+T=DBTiso zW|0w`1@E1GMFz%tdOUrn7+CpyH}1CY2SON#<^xSJ<`xh2@4fs9$t;Me;cQnO1nsD< z{&bflB$k)fX1Wr|J3chf(4wHAKrZU?)uh`vT-gzpm9>bhfpgp*sasN;8&PlGL}))p zUJotTtx)(km@l9Ag7G7$h6W({`S>Swb#?p|4?)vnkiJ+#XJr_eO>f>5c4-2^sVlKO z2k57K@#2k*W2hKXZ?D_JTr^xt8*_m|&Ie2hk4H;}Lb=u1a&D(xA@tVcyvT`WW}z61 zcpr#n7c5@zB|@Q(VV?EsON);)&z?EYN`T-v(AOUq{PN9D6)clqK#1b*4uBmz6}!SG zvZ=8Vg|42;N!E~bm*(e_LJ_x)o-RR#mtHJ$Hv9|%2dj7?>-U!2_5(M9e|tQuGmR^x z%yyp--fRpE zeENz%Vn6V(jElm~{StR%sZI^d@zCY-91wV!Dqz?jn}W;u(&GxWwL!lgH2_U<)^>m! z0LZ=8*d-$HKJh`)&PyZq7It%^vApvGMYBEYjEHI=thld3{>?8>L+et;EI10 z2O1jlIHYX zT#7B69Ur^U^_AgnTZ#!2CD*PU+q-A@;oUF7Ghe@YU*YZV!%=Y6)$M2>VsG!pY7foW z_#~mTJQ9)Naes4jlO1omJsNTncR1}Y0RKj(iPKP95+WCt5g1rrZV2J_?=0&>eVHN8 z(INrE64{*~*s1jpP$|x%Wnp9I{FzN&$U5H}Ei1yKR;5eP_5r}8nZVuzgTMQ1vxF07 zS8!er{>@jkEpQ)mx)!et<_d6NF21(PDr%SM%Tiv4gcRpNw;z8x-}C3I?MQ{Byl@|E z?Z0xk2wx1wS;e9KC(FCix6!*$2~)G)Y|Pd=AM!iHB{}UEA>c=^9wWAC(VSlGTk1nz zuM~evSV+C9TM$MTHYEY8?93}0th3b5#%>rcXyTv3_`Nz0Hr(;W zW2oLfzEbBa&a>U>${P8Zri4s$KG{_%1seOqeF<$3{zg%DH8iM%-7c_fMjDqdkQfZc z*LY>cRa}FW*XGD7@l$uWA(i!|?;z}0Lj!g$y0)|p$Ak^0aFb{Ei$XD>t<5jj_wSMR z2c$={b##eU+6nY|AkyX6=h2h+uSs&`UFl`+D3Q60`UJ)`LQc*o&lBD-oOBiBk?GDF zS8{ONQI_8Du2l2?E(k} zyMurjfEjH<_`&r8p=1rLSX-ikf1QyZHj7W=d+$q4yT9|IUSfae25))1(>3v)cDn`= zhfeqapOwThA|z3c>sb6RR;|zrkJOge1xgTF#KL+>!D&=$8+TJafa^y0U;B7A2Q%F? z;n3DUDw!TAB5Qh9=_9Jq{l$_f!My53umb}YhN1FX>qBS=K}q<_GZQnno2$<~c=*WD za^h8@S$#brvMV)FCu7L1(cb3s8X(c%=B40U%*@Jy-V~iPe*xtHWXE?OYR=XLF#=fd z@bHNGGj-V!y#1-Ek6G~Cu10I}MpvoTAUS#YFHD0lk%5sNk0d@Xp{Y#rdazLOWNvOQ zb!rN9Y;n7GJ%@IsQz_T>f>Wo&UboCmMl{=`d@Y{54st0iv4j9;Bm{Rz#!xqQERQk1 zzv>;*&T$zggU=kS@ElhGB}`RwWg;fWo>Z3QgktRHlaV28d3aF0z4lGC{9EA#6y=@;N9w#In82E9C*MP&Tm2uG5~Lnfk8LANqefVY~)-> z)<$PP-;3yIs>xzgOZ}}h6_aOn-GvWj0zJUhlyo>%9+i}0<>9eK@*>aAvu1Bv#Em^2 zF~c4XHGFUQ?EcYzoX7YhOI88z-X+2vFi*sr_kMc?dwcO1+J0~&fNQH_vvziu98=b4 zFLGL+kHBJ)VDhxJHPX{dwTZOtd`PP3J2r%t^2kKbMVA7hUQ=`J7X&D4pX-}X%XFJ2 z+&pr6ZoQ+uMsNY^Jku#ijEacZZHX>O^f<{`kooqlbSkh9A9ZuO^C*?PZJe$6s0;dEpSEztc2M=1{GLHfcK5fc%SZI@<;^ajG?1d-%9 zx(c9kCF~a0T6*RcKYGX(*HKBD-xtwA>@vJ}zn=NA9lmfL;*7sFW;N+`YsB2^(3`uwh~8QNn50)=>ZPDxpY zsU*#`5qsM5!s+GnZz(0+W@KW5+LY)tEuZaeU;5fZqkkgooXyMQJGY^%46wk6{1NkZ0lpDRWgj%HQl0BdjAHj$goa#%>UsK7y^&O|bgaud3 z>!(!@;0xN>noXyFXAu~2AyP2h&1pEser^E$*U@f5PlQgU0TGrI6gu--%p$R1vVPPI zY*6X=0P65ZpLWcZe_*|Gm*+euYvE3IDRu!+*BT{hvk`MBA$#jcIJ=WWv z(Dk{pZRBl|ZjQ+=W{r)OgK~!U6pNbp!ye-4nfTB@gqVNgvD77Q$O!L;o0p-k%4kq# zVW?HCDG07xjHFjPsC)ARt{jd$R+DQ5RxMK$haq4=VFI)3BiA41FS^EOk+wu82SBKG zu{RpOp$_c%P=NS@11pRRd)eH4bfWnU#C#nUY+#7yi+})j5zpQQJj(ybcUJLx_XFq> zYlBeRP+auHOe?81S&6ZW+U1?0v%UR0ixjq(u(^g(s?#|;#JRH>dB>ilmK7$lch3|4 zQguL+Pa1ST4y^nzO#Fd5+69S--99$Iiu>*sA$@-S(@ODARxgEr@e};N{Lzu)inBk2 zRp4DZA=%iQ{OL6lCu?|Utk?*}%z+u+I$ipc@#)p+!Bqh82V)kLfgA__gL`jcu^mb5 zv;FXB(>-(ZIFM$M$hF)%x=6DtEl7^z+!|U)5kPjE_ooI{VBhs;ysuXdF!MjQ5yA>J zANPk@I54-3m?Af*nIR#qS#2Fq+`L~X=v^a9{o^GXUC9EatxYEUqp zM0vs@OR+G)?lO>)6r?i>iwnnl%nLmi8Z{fAoOE6BUh0b)PXL|PmH*7Wv8lXl!4_2? z?;y>~D9V;IE)YIVD{UX?!~3=eEnL_k^zaiHCcGxT@(-auS^dp+zNPj(`N5AAlqu^M z(}5wt2NCM5E%6wsT>B9FlI;FJAxS&)KND$JlIo^^g0@1}+7!Uq;OtOSQv*jEcqcB( zil>*`mw@Scr|hw@{-x~{yBMOB{E3!pLUcx{x6@v>t`dPH_1^Sbn*|4-`ZvxLipLPv z81JzLf^TR6ErR5+i~(=ry+loQwnfu--u2C|1{Le8&PBfM0#RKji|tv&ek%(Lb}0_1 zh#e|YojgQ&0vQSKuf@XYH+et=by{DQLGL^(PQv=tNG%>ExGztnhs(vwp+AyqT1?er zcE-b6y(;j)gy2Rdqad%|t?Te0z%KU?N{vYe*yBt~PD>kct2e+UP zK{4T`=V5$e!kT~wx}~zRHIN7B~vr z>2TkB7@|wi%V3idp{V2up|_-WK7Jl0r>p z6QTbM7=~bs&|4km%f$}DchZ;+2y#eTX8yR=9M}-mx~7_oLpk;z1|_emJ`*yuPG? zJD=_=`4?Ku+_-#f!p(VwACC$33or7_V!0H?qMoo|h*3<`;^kX0heF`gJ$)vk24(nN zX*M%6Gkd5M5|cs3oOOtodj8x`wkahk z37C^aFn-k>%h7E;!!2R8qKEdA7rCDc^H!P5OHpvAl{+O0zE%!C>-(VdbL9}9S16il zJ~wr1?_p+T9X@wkCCy_{WdyfERh4{wVlaEi)b>4YX6DIyPOonYgv?k7SZm&@X&Rr>&glB27-Tz; z>ZXWO}WFOJ0I5LjmAmU0N$6Tqm--E$&CXJC$FWXQt9 zR3k4UiB}@sylcUlG}-hL^O8;av;9x<>gT6iLXoo+*Dy>p0(g(resES!Hf>)`xH$!^ zhCp?RVI-p*jVcEj@DlJx>kEC$V@tVvDW-V0rIHgiYRWhmH{(6_Ad3Fo+qc7GX?H@= z3MLYPJM6Cq+3M}W_|+VKsi~s^nKW=&5>yVf+v{>p_l3b$`2|~6vG$>} zo2{AEC!JTVYbT0=th>fWcB@8K>Ps^MUg)+OggeivRpu+IQ{pgCtiqbSTYLO=f40+q eHWQhk?h(fSs*>@7Uk_e!kMebmYuWO5pZy<^e;jE5 diff --git a/images/ssh_client.png b/images/ssh_client.png index f1a828324b8457a75bc4e70d51b0b0f8e6604bd2..b8cef4e4d978e4612bbd374de979a75f69b59e6b 100644 GIT binary patch literal 23299 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 sz literal 21447 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 diff --git a/images/tool_diff.png b/images/tool_diff.png index 6f57b2b834b6df3c54dc96a525040a16975dac81..d7f209ac5ab05bad03a00399c103f3080b222a7e 100644 GIT binary patch literal 32622 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�FPrK4w`W>oyaPs#I52(RsMGZN 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 Y)Z9JRG3xyLZRq#QODluQB;S7eA7m6IW&i*H literal 22727 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℘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 Date: Thu, 24 Sep 2026 21:30:26 +0800 Subject: [PATCH 029/166] Redo the README screenshots whose arrow labels showed as boxes --- docs/updates/2026-09-b.md | 8 ++++++++ docs/updates/README.md | 3 ++- images/tool_query_json.png | Bin 20004 -> 22060 bytes images/tool_url_builder.png | Bin 23574 -> 25983 bytes images/tools_montage_b.png | Bin 79943 -> 79899 bytes 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 0f793fff..2e04705c 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -887,3 +887,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Change**: the three redone at their old sizes (1180 x 640, 900 x 620, 1180 x 720) with the same content: the SSH tab disconnected; a real run of a script that turns a curl command into a pytest test, through `FileRunnerProcess`; the same diagram, imported from Mermaid and fitted (67 %). Rendered at a scale of 1 (`QT_ENABLE_HIGHDPI_SCALING=0`; the machine is at 125 %) and with Qt's FreeType engine (`-platform windows:fontengine=freetype`), whose greyscale antialiasing matches the old images: ClearType's colour fringes made the grey edge labels look orange. - **Files**: `images/ssh_client.png`, `images/run_output_window.png`, `images/diagram_editor.png`, `images/tool_diff.png` - **Open items**: none. + +## U-20260924-298 · 2026-09-24 · Arrow labels in the README's tool screenshots · #docs + +- **What**: in `tool_query_json.png`, `tool_url_builder.png` and the lower half of `tools_montage_b.png`, the conversion buttons read "QUERY ▯ JSON", "URL ▯ JSON": the machine that took them in August had no glyph for the arrow (`Query → JSON` in the word dict). The IDE draws it. Found while checking the README screenshots after U-20260924-297. +- **Change**: the two tabs rendered again with the same input (`status=open&limit=25&tag=api&tag=smoke&q=order%20PB-1001`; `https://ada:secret@api.example.com:8443/v2/orders?status=open&limit=25#top`) and Query → JSON / URL → JSON pressed, at 760 x 460, and pasted over the same two panels of the montage (the timestamp and hash panels above are unchanged). A top-level window came out 762 x 468 whatever size it was given, so each was rendered inside a container widget. +- **Checked, unchanged**: `tool_json_format.png` and its panel in `tools_montage_a.png`: Format still sorts the keys, so they show what the tool shows. No other screenshot has an arrow label (the HAR tab's hint with one is hidden once a file is open). +- **Files**: `images/tool_query_json.png`, `images/tool_url_builder.png`, `images/tools_montage_b.png` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 291f778b..6d4c4ff5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-298 | 2026-09-24 | Arrow labels in the README's tool screenshots | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-297 | 2026-09-24 | README screenshots of the SSH tab, the run window and the diagram editor redone | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-296 | 2026-09-24 | The diff tool shows its diff in colour | #feature #ui | [2026-09-b](2026-09-b.md) | | U-20260924-295 | 2026-09-24 | Automation keywords readable on a light theme | #fix #ui #jeditor | [2026-09-b](2026-09-b.md) | @@ -379,4 +380,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 95 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 96 | diff --git a/images/tool_query_json.png b/images/tool_query_json.png index a78c8b2c964b187c1ded13e37281385bb3ce0f01..e86a01c73f422b497249eb0774506ca3b848496a 100644 GIT binary patch literal 22060 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 AX8-^I literal 20004 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 SU&MRI$W)ay6pJ6feD`k=0(UY1 literal 23574 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 b)z~`TvdMg%!*xd)ka|j6Ll1&gvwrnIS%()v diff --git a/images/tools_montage_b.png b/images/tools_montage_b.png index 91441dbb8f1349ec8e1904ab823c190425b2edd1..440d7ee6365496890ac9aa62a10ee5551b621820 100644 GIT binary patch delta 43093 zcma&OWk6Kz+BS@df`p&Nz3ULqdsVm=Pp^mKs8U$vCm(I^=AtWca`Lo zYBXsc_0_olPEF)uwBC}l8c;>}f7dN|_K{1NZq(VrQ`TZ2-YGqcrTHpR!^?*EH;IV8 ztVntZ2Kak*9ld-yBP!r#5YDQ`OH(KX>7{-V?=wU$=Lw9|`?{=cSqTbb3=HBDOUrT? zxE?!fy+5*0st%=0muka%A4FObr0l{iVqj#aqFCKbAJd22X4_c5(H_}LGLq@_%zctR zU8cq1^UP}U)d!P3D^K}xL6h7MSQOK01U);cYbbM(Q@wbrBb?9M9qUY&>6Jt|E&KH; z?4q7-1OVTJ`{)|`haVu8js|hnC_|xIx5Y=Hucw69{nUZ1n)+?3U32@iCo_Hu>GUG@ zB_}eLLu6!GsiiYbhkZYEb>#O@Aki54?h&>i%8HF&Ojq4DyKZBTE-Sy_|bJ*ccg0fDlAQq z)k@|b=_oq`A2P@L3qa>?cJq<@hk8ymLBKqVCG@pvx44t<=ez+(^r+IX7Sul_*mw9X zF89djPNF;MXs%cWsfbRx_nOgZ8%JfZg}OJCUN$4S$p&~<7(GN0&0Q{_si4~A!ot-%CEx+`Wr7fO@P;j4&<4j%ltN<^gX z;kQ{ZlLK5IHcJ{=?|s3!}&_p8@F%s02fbx?{Stu0hn+b z@`;1;uQ=?%Gp@WwH*YL9(0R{KgiL2V#TaT`lwt zegJ&+lU4%CU`mUQ!Rxoe3HJVWqsM{_{N@Nlq203~`a`p{p;ub{+QRgsTY=eO+iS)Z zkEyL~FaO2)gVzIeKvpB{8wqJ;%N8~v8NE+hy3dD{Yd9Teo>;t-k~QxCQfmD()?rx2 z|Cp&*G-xSYT^Wm>pMcB;w;k-(v_p+X3Ba!^OYdjcH`o;3HGbT+qrl1Wwy@!njUK_5 z)piE*ky8DrwLrS_?%c?GujJ84G;3(F>4+01XQp2?O^6rO)tWrEUakY#2&Ihs+EkO` z{DCtH&pwvxPhU_>au(*~p-P~C1vM40Ppy=*ofQyr?k?M#7ZZ?h^QzFhq0<^449skN z&XZFt%ch(xiy5)>_#R!TB3CeI9v8>noLDpzF48h8n23ej<=b-T1#7~%P$6}MbOiQQ4-n?j%;bcmtq&)gg5nwcQm3NTyuBtFPt>{UG1 z4l^VNtEO7a;MVFJKdvoOEs!1ohjE7)O(eIQ9Pb@d(G(Wha%5m^x8<@CrELOm$t=iJ zCgtEuN6|zp^boh6=N5Lb=x{oVrat4dba#%FALYSx zR8{q|Q8{k}M3K?14B5bVz^dqB>2|WDK`U3vu!@hk6(%wH>#eL*kF{LDC#+R6&F;DV z^j(41&V7ljo8V*oNnLZnq5Ovwn$t%fBkK(tk^@aDY zwy*x3vg3X5?N^EElcJ%-X!7iy-ml8>zANRY z!B%X?!3*R0g)DI3(~$R>znWYiwKQO0*IH3UI%OBbc)xf|4y7?ep}X5ueCn%du&l*q1~GKwPga2!%)lGxI+v(;`L`X3Np*)vC*yL(*pXz z)kg%iClVreX-m~q2UQ$yvha1y_J#H)e2>2mSWdgfD0VUgt^9m#7oq~d7~5jqZeFxV=8c~yzd55hp1lbY)+mwIgOl*4aVur}2Z zj+muLEU{Fvf|j-or>MC4)PYa?wo=&jX;W_TNXCFnj(|wn%jpKOA`j*~;lZrP)_e-#sxVV6pGjZQ}>FLR#u9o z2U9?Sha`ViYJ)h!PH$?rK2toylRnGy8IS{;^*~&;(+eF@>#HYOm*npPW)INz*QylJnBP`9nGBd}Bu=E~8PvukD-5NG5;Y&Eg2_p>Ry|tsrOsKX;^xV^ z?!~F!ue?^O>he;`-0sMHX$oxp1!16lVV%z`Hmiz+#n8EzVH9zJZY-I6&mYkAyizk* zuB>n13C&S$(M2Lz(lg-lYJ%D$1~UeutotqbD$m}>Wc6k@2_xE8VT=-p7`49iX093& z#&#{VpgkD{tpKk#*3NSx>g>;v1Q)#6S~|~P>MctRZbN=~o>`tocJoGxa<1AhI;$lE z=#-N~%c$fdb!x^2sE_xoCT%kYG@JrvsMP&X>gqaYL0RWd0m#KAer)BOZJTeW59`yL zq@xxwMhq#pc`YHwmlmxCHR*-qTKDnc?pLm8=D^qrxrV$Kf{F-XqOw{oS-+*s$!H^bNRe4Uw&iom?)G_vGRGV`v}~>fm|t5IZ-jv_ZYFqQtw#a9X$H#VYrz;WEe-=K2HRsRU^l0%QM3t*X z-0vQKj}#1}lr;HBfB$|;{;iC_hVrLHnBB>aW| z(<0M_h=XbL)_$#6eY2jzYB%2JZ-0uZy>lFHDjPDi_Fm`V5iuDY+a* z{7fafPvw3WWX5MPA1?TRKfqkeNU`*Kz3C#XJi)!mTxKioA-N756fg~^27c5H322>_ z*}$WH)Pm`=&_{=H_YCswiV3p1DfMmgH%!>8%TZC~$v-O3BChs{_ta6lUAsJY z>TAMm7PB~8r#oQYmYURb{I&pe6N3gRwRE9M3Geh*NC?lI<2^U;gX6;_GciF)rk$Pn zUs&$?n=0-2NyQp2jR?oapT{(P&@e5aQk4@>k6MKeoCT- zZTEljy0>iCq^@T!&dS&~ zh-*bfAvkQG)zM8~Ti;y#32U?9%Y%f$PqCO!{Q5?N)W>zP39^Ch0Pf?rT6OP*(vRF9 zR1uEhC%l27l-ue4bJ2M3q1k$^^Iso0nK4I5NnYXhhsiE|nHSI*J3S;_n)C)>3m^8s z_Bfxz)6lMB+M17w4O*(KYA~rqelK3w=jmC+Ba?LcFv@gq&Dfz-Ssso#wzcxUZ#E^X zKfTuZX2P{p7zu?Jvnnq)Y5O42V`97gupy#^T2G(TwCMUyCn5Wb_3Wr%u=@WCB>#(w<=Ty=>m9G|ez_X6C?2Hb*Cm z#8EtHRMva(AWr_njg*uUA`FrSE?p)fQ3PtFz|uivbTUL5t7XW9X3Pm#eoKR2K-r)~ zdE9Yd%UTbAhMe0P+^bUQ3rDO~t{1WwaGh9dpzni8-(%|-k$W_9IF_05TD$$p78!66 zwd&P9Y%G_V=zpH$%%U*SeAFIreV(QO9{=T|*=Oq38J^H$%CAln-%qu#v}5nDwySW; zM-nXX{xT4a;FpJOWmB>$&i^+Xinr>$E^-S@K9FIHvj&B^MAkI9ilH7iu6c%-=(_KByHyqlZ9+4;L2j&3rDA7?2-fh{Ms7_N(!i->35696CYm<;!mu`$dX4Xevig zFHCTw%}yfKH1!VW1WMlrE9}0dOF9PM+%lkURmF;ABO-!H-ckR9`z<=Fo7hCi;LJnS z7kZUcS*EyYXCveU=6+nQd*$koU{f3aDPCLKDo?q=p0+fJw zpehQ58lFt^w3@*deQv4$MY3|nDh{6@iX?KmZ;FbEF-c@uOjF~0 zM3ZhN@pn3CrIV&d7}w;lh0YroLQZWALATHSAh3Jgb-~SVc`V<5;yB?tF{5}~kZj>E z{8gg)I}M8D0JtBLKj-lP>QoU>$>g+@m06ex_ZXxPFt}&WKXd()hK|AhR=Y&lqi5dW z-*@!x;ESoO|EbBRo zzWzg@wrnkdzS>uI(=jHCF;d?$BOZkoTdKf)^?TwJZ$sI`W#@mTTBQhso^^jO*I`u4 zLCDBPyjl%Qb~sesB$0FdqKd=1GxgUQ#(zd^^4}x}_?zV%#(`<+kq3}xmxa}cq+D9) zJ3HZRwXf{)at0wV+}W#VB5Xhjt>8(%YGUZqq1{LSj^y!t3q+=I)kX60|5 z1bDb^anZA-P^3T@dmFXY;yWI>bz*RWC78oPFnrS6_XSkPL$N6 zzWI32WK2~;Kz%2eKHhKr5V$p0EqO+v3Je6PYbT642ZTagN5u| zL1>rHB`Ay%qyrvE^LmW2&+9Mqb>*?KI{wv-kaju=w=H5V<;|1OUFPVAc0ml%K2K*0 z#El^GH(5##9gaJRh#nBBC_L3S8l5UFwQ4v)6$pa>krB^nt#5S!raHDFD4!|`OW?=R zPz>;j)*-R)~Hzb~ZjuEe)f|KkOx75N|v5$iqG_ zS~81$r5v!=C?eFkPP@1#_SQ$)6SER(WrfCa!E6j}@jZODa&Fg)rF7caIcu165M6Yk ztT%^t72e28Sy}olK|x9MhIEnk?{u5>LoFS~b%=&tVdGOp&vw;1CrI!<6L@X$<8f%@ zjU+}2EWt%d6UD;E>9kWiVCQ|y(`YT|qn9-hHM)ylp=iQU`1S$_zQa!LdhVN1u{!A@ z(l#BnX& zQ5(y0iuqu)1#y$0uy4;U>)(%BK1=UJSE!h-x+;67R%_k>Se_4Rf&tVHn54J}ij{94 z&yjy3)=2^B)W6siNUqcDj&(ERaW9Q4lvaki_FhQ9B@fkm5D=8r>*d3axtom_i7p1a z&|GFQj&If`Su*aA)E@FEuS=_88Lih5h_5pW%3o4sr<%#Kcm}8qYVz@H&C*xApf8F3 z`T+bZB_!DH;}*+mZ|&Q7?iQK)L4xI;-e7TPJ)G*z^|NTFfUm8XT=zN5Xq8@8&3zcE z?L6CjgGF?!e!B8KCp}vwmd_bv2Xs3A39+?rGzuw~et+0!X@(xW?qEsyK_n%o_<& zoe_qi0&ypso(Xk@R#Z6f%Gv0Sb2^y>g^+|s-OMblZP8dM0H)K^+7x;(| z(*QYEd2b0Cj(>=c`^ z0)G1si+C9N+5kYN;$#J-{tNm4Z*7W6La;!ySdIYi-0K*vJ{6Mu|L$7+-LyzfwB3Zl zu2fe~V7dxcR7#F~yH;K$(#@kMcAoB9!OVmsYS7B{TpjdUIB;8uVjK`w$36MgU5sEI zd}(JkanFo5#Oj#hW}=gVU!B`d>&w`#RW(udGn3<-a_ANciXlM*uu<5I84eSy9xE?psgfVnqVDZh*=y(Zr; z5+5k+1BtMs0@f?1V=i88Pj#-WbAw#2SZ~6qov9}J*;-)7B_bGQ#h(ql1`!^_Yo^m` z++@9IqaXdsD9<;IpDtX;1V5UUT<{hi;4Z+iYcO$_mt0M zluVP6k$iRb?Td3pDvYZ=z95-&EUVdO4BXj%RtB=_2*NCOi7+C9wYgS;hY=sra9sjX=Ve^y@V{g^w-)QL;boX5^EwJd{_q zxJ2~!&JU*K>1T=b9~;0ekwlJ88k&1S4W1kPETP6{8aJvy+S0K3P=2KV85(&Z|L?;X zI_YROqg%RG&0HwNekp}-_zc|QKj;Jbe?7fj6@Y*?tKTSrBEEwlxP&9aD# zcX+?^6i{8-pj|9TIoJ5P!t3`-420!cooprW3zjZE{d#6D@z1KN>OA_Ze>a|}b<5m! zMy$YcG3}!~v-aEyyn$=LpP#-6|8^&eTjU4s_QQHa%315&3ZdSx8x%`uUw3GfhAsu3 zHQBAsMJnHs`bE~V=yypa2kqaI{H`rAZ|Hvqz8ih(9W(mHc=_RfcEbwmR;uvcEx+Fc zXoqcx&90pvkCu7PYZ|X}kR6z<`(;k?KCdi&aDJ0WY%hgx|CdMblS4nr9NX`Xc=2 z3y#T#;*Fdi`e;{{(BB3TW){xz;8ikC-Bi|pNOo_(1DE;z_@8wL{KvX;bG4q#>?*Aq za74_#4We~gupX4`b_w}&bmu=u&;8HQr%Mmp6v-35JfKgYged|WZa&0sk}TQ%y?)7z z=#!kJ5L$By4TdJwDpeOM$ptC1JzB8%ham-O`>3VkzV~%erBPh>@)dJd859(6)4ZD4 za6t`NIWH4$q|dWcq`xqODczPAu6*>#&kVlXaMq=a)}Uu{t^Zk(+qYWb3fVY+ciesZ z>`1+C88_MRm?aeOg+VL&^A3LRIzu6=JVsV|)1njT95vL~uGw}{Gha^g6+gN=^2Mc* zTGVFbTvWl;tOb^OSWL+AajA-*kMsfT?4a8aay=u7cc{BGdXh7Gk~;=rF%HJ9*k4n% zh3ChCa}-X(k~yx_>wHilKxeB`DlyPtH_;ywkDlk<1iVK-c%l*u`gWBnn55R*Wdc$O z1dh!tf%-vWk9r=0IkSN!>bO9J&qSPm`Ff?@++7y)3h4P{X~-ez3b@Q@W|PB~J|AX- zfSBs0X>JJc*6gzT?WlY1x7}|aJm7o3uKXnF0Wdq>OH+UD%`|Gg=V;M#u z%0TL_XYy>t2VvK@I|YlV{TeLOPSRWwF3*b>t(rk`gGC`snX|OzZVrzRvmSR!ze$xQ z&~JQ?smvf^UW7frL8|Ng?8^L|$Y|}d$mq!E&-9=ruih+-&$X5)aiPMlNE~-A8YUdfP96Tx!yKi_PY<{Gw4I#iXSdDMm%P>1I8!G3fM5cCWdvBhy!5lO5dVhOR#Ds+F0@Auf8fog=$!)7Cn>01F`ZF zSE()6S@`#Zbh51I+sN}5RPyJQ2^Bc1gMOD46Yk1nlXE1y^RVn=Qg?3%hPW9Wd7vQ| z)E-b^AVpHFcVpj1PbIlCE!r`OME6sl?v8e^&P(-{4Q#DBG)m!t!GWT|=W`&a^W+(7 z&q0)>BRkInw>HXX`#9i1fnUSaj?VSAVkbkhcv}^n@nmlsmJT{vm6U| zl;l2p#Rd07h1wv-`Y}}qLK+Qz{xEf2us+qHVB(Y=D zL}o^ai0@5Y^0Wf%eKvbW?;6i>#x+qEg$O&k3%@h0D#kEAl(J}u3Q$B`*32|mith-i z%OAmeQB0-481!+U>dW!FM+YIpY{XFBVm$)m53>}Ibh8>C>iaj9>Hx%Dc?wRw_5*;)9Pbth$Mv-UY`~ zJ`|X$13r|<-?+V!atI8EV^neW0W$#1=EH7q{Y@4^WBzkD1IhZ&VV@0mw6Qs@iOp>W zk3);iHR%XVLj+urPzQ4ncx#Vz<8RMt2~9hLF|Mp72+qox&(8v*9*5qz`KGAuC)vrg z9O_);8ho%gDAvn})?qo=d;PYEgoewQ@tNJbZ#EA44S~>V9KgX3GyAK_#Oq>S@*8wM zH)g)NLRnJ}p6zDV`0O4Z1*TGJyii23ziDa9RtM*b&51@;0ToO;(*39A>X+1@-|JA? z+k6hF7kEHq&T!WY-Rk}yc6i@#W9lKFeB6T{`noo$f9$o1O6I!vI3z_pSd_hqWXKyJ zse*UKk9iOr;2FYmY}R+i$pR`Z{}T638~vKbqhftjx(3s{9l%JGV8To0IEeYs4;gex;sO8JEIpD1LdxF?`pc18LI<2 zf#Ox}SkZVZKl8Kv2!ed(@o$xh&c%fWt-;zzYVQ>xnlPSdgQ@Gne#!3dNX((+pi9QA zcV^1&?hujAF@ITkz$ahzknc{1;v0GMtHbWm<|;2Q%yV_fOp?PWeQmSe$PnACNNS}5$uDj@X z+w2K;zxpG zR;h>Y=hK&;JXm$ep6p+U%zGf98!^fwm`3Q=u1dHS9)1dzz=0&ySn_Mc4|fu$od5i~ z>xV$x%O`eJrCA=AfX<(t_7DCb1S?1!rRd|B8XYudj`bwW#NvZKEmZ;rfYM`(K51jH zNJ-$SNY)hEBir?KofgvcA;Wnp#}T=nPq6Ts;8xwV8>#rf6Dl5P>h0dJ?WPJKN@@fC zYQvFl{<2cdhL!jw?}z5xRdm1C)?f?`W7OO!l2y$%$C3EsCX&b;8g7vFQdbMp53Cy9&3*z_WE zO>HUBlsVBje)dQ=pmq@ViBv;Jd4k2R#OSYGub6%TqEwC(9MT;vri5`X0Heo`%I_qBSxSnyS` z<%%b07FP)2D_eds%cap$9=Wa)7-oHA5i!bjtYVD>2D)wUSfS7j85~8nY#(%-FF#i z>;STe)PUFhw(7kh8V<5Q*f@%lZ;3YH-2+JwUNQp8BkZZx3HmGwzMu71M6QUX2GP!? zcrk%MTJ+Ds=4FRVUFaJyvi~`opO5S8>eyF!D5dqn8^%hR`O%*t`F`!*OK{dKVl)Gc zcv3?3$`rb#VPEp+_PfJts#m}-LaaYUA1PWcAC%We&~LPd6imE|Zu9tkT@MJ0TqqTH zv(H-g`3YkF&&Yq!u+-weF@NgP!z@^()mNVzh){YnOQFf9(Uc zZS!}yhR%bUv{yew6A^viF8$_y8<{Z7Z+$!e%Qr!>W{N*e8jw(hxnD~l6vjV@Z2Kmq zU&>&>fdBLD=6|1RlyDpN5{Mg(e^IK}oNQSMpK0Z=jJtgs2XQ&XVES8i;`1fzoDgMv zAT2V_$+$Cp|CVK*?1k4{VI9H}Wg1uxYVFLpmaZl|R^j*FZn5so%`y|?$&|xYO~&4J z!C#}pPxMSRL;7n*UOWlJVv8UMziu*gI2?{7Sj?3w7*NaWe<%f{Uk-?wOdVqV#S=hgotxk*m&SSkAP<7wEe znkU^QqVL|mhsQhMW$c9M4xM4_+VanyF_=9Ml-|O>!-rA+xjryek8iGbLDstN4D4oz z1bw%Bv7DY110dbg>FqDyFtb?LbD6AlSQqDc|=)R+2rSmv%(-$BIcy`$Kq|RfPIrNlGW&vh%k>i zV$pkCi5G_(Rp7abI`i`dHcXr#n{CNee}X+bZ8={e%?XxuT;Gv#ci)FjRb)s8P0}N* zZp#DnI$wW+@8j0>kk*x1s}wOuW2+lFU*)mAR9!pMEj!(QHI7rIoCMs*V&uibZ0*ie zNkZ409OE5%Y0TDGG|j*u!W^1B_~&)KI&4ZqYBjCzaLGsVq5-Fw7L!UbWU7F-?vDRX zx#a}mX7jh0-U_2$wiC!g9HEV zq?G4D`0dGUsimbQG1pl^yK{(}7tqO+dy}Q!6}kTd7jpR=e+ZpvH1gqx1id^4`fQyH zpU?4clar8m1+J}0&&fxYV&-L>W>=7~GOOx0!O?aahw6E~Hm1#vjc9?(L~qZ99jE^? zm?#;u_u1N8@!oP3&B!wbZthlB*2?7(==QVe-B=AI?2X)v2NyN0~7N&tF=Q8ij^=Vx<< zv9iIxe=o*83MXYrmz%d6OD;!hVA}#>VIEV5!!v*<%&Z(Glhy1)f4cBX%rc-L5bNR;HGT%nf)6(9)ecSb7eON8; zLiTX@3j{e*HmO({`q(@0Bn%Ty9?%*9yMByr%ywaB-rYwAxB8*$m1d=}+ovBM$hu!*WX*X$Up}`i6IiIy*nl8AU=drZFc}MRty%_fF zYyq$$1E2Qn7*%Ehi2SgrGc7kn%Rr3w4}~-c!KUy_yAGl+E4!_$Ru;rvf94;3{Md@4 zc+AFz@Lp&1cAOXw*o5Z@aP#uI98S7)Yn_7vnm2jZkAs2&*#pH`MMPY730I=2`Tf>5 z$@#d3pXnqrwVt^m+VTMSg^PKON5>w;qo3_zl$S0MOiX4rH#cMAPoi5HbGAxK&-NqZ zc+-hXd>=!6#)6$5^nM_;l5bfor4cXhz$>=yr1)YY@!Bv&d`2@=NmjNTM zd&VY{%jYa&Zxa!{CLt$x94XsG)uH3#<00D%IQ^G=)^xbR6tF1GRry)OPFZdr4mH#8 znR|IEVV+4Sa_)W3kS`ZT_>vD+pn(^@R%H6Nb;o4n5XruO|RK!&=yRH=fNH`J<5Y@ zTgNM~k-}*&NekuI%FfQt!I0UAhzN@ouiiKjDPU2)vD(%`e+L+F76s&j3~~0^!1mw* z;SvE%fky`isu~(X_gg%y=4=6TTUE!>SN#S3DR4%qmp!;EA(yWpc9WhtJDVH7E?&+_ zuVK7%Cva7@taRS+K5Ns-U6%D^d z>0EKi^7?wgh#%_Q`#3$qRMc(q=m%J^K&|uxBH&NgQlxPwmP$VKDzUv?I?@k=2i*Z* zje?C>0biTzJusbJU14G2s}POT0=1M!j~+1qt$Fg;h6dlk_+hWDUz=$$C&-`{u+>US zn-*IaY%{)|Gb_d<6>PonO{*`39|D$=Z0x$TscAa?slE>xYT{F)$7)~F^t?Bp2~dB& zVpX(&+kL7ZhB8PKjC0>N5_25#8A#$@Tr`587xnmf6 z%-4yD>zsp+&$9AYY!r3m--j@o$v>07bLWod)VHASApIbw8~1GAbILq=wR3*0 zJ-o#T?wa-G%l8P=x{dwk7zZJ{n=BO_U4=zOatCj?<>cU+!(O#HUop($9HG9@Zgg;P zFnGW7t%TU0Kjja{8=Wwg%vL$|v2~UKF7Hd6X9R`dIr>FDym>!fR2i4e%m629p9O^m z@Aaj4N^8tBMvyZP)!V^OQ2lHPCANol7(@6Pt$F~4xu>Vc7d<$o!KSxuDv}daxLm!; zEsX9Rs&89pHA^b)W3xi;RJM36{_Ips;b+Dk4ihuKKG&TZ0s|7f(wLAI7=+pIH!M<8 z0c)9lA=g_F`R;k(a{+}E!kaiatZ+DU`E+1#F=4pvwRWXu_9s67<32V1vs2%x5({)( zgju;6e55LR&3P9A`Kvg0F4`h6D3DIHSj_dvXklT&udr$+gyOEFEZB0O%yrG01s^Ej zXrDahR!{XR+$#`;vhbjeww*gcB6r51<}=l8va;JbuZ@ZTpFuFMu9nz?Y1hKicPRMj z=bG0>LP-nBs&`?p+!W;^D`#0rL%1>H+y5% zotX398z?ne%+)7|XNw`PhyNCv-4&vF^Bv2g?ZVwcbu~4$i;a*Ui;Ig<*0}Yp#yYgv z4Hjq7xyU%c>KMQQc;rrv!(sAtgE&iQr!^Kiw@Xg9cj5-;8B`uNJ{^m1XgHNd&b+Ld znXH)8ZZ;gOS+DTZ#^fl)f>@_PD*~XTraKv)sR;?cRipG|ggY2(-NA0#pUfy47@83@ z)YrEo_hyecgt+zGIz=V`lLbE+!ftJCE#R-;0+@tf9QWC(sHjk}Yakl@pldYIWyX!D z7?M@CkN7U+@Wt?}?giS@w$f4>K1=_+^$WrjdQc4Mt5G!*!zee>>;}|;a;;Q}086_< zY%2uGNk!|oztpLsSIBEpKi@c;n#zs*NP96t9D>+PDlP5p?d=af)44O6xNE1Q`c+_hjNK5WUFn0sEqzOl})3IR_6BFA< zkA`#LFD}q7w9_p!(8_3P3kHqh*zxZexgHDgCr@7V<}&LmW{G110;&hF#+f)w;Eshy z>`#eXqG{@XgElo15^lX>MlORGj~W~0H+x1_f8Z)=ArDBwQ#Yc8e1E+K4JBkZ+`2`i zrny$O`gu0Hx6faHr(vVeQOTVQrW_4N$JxOhQm}b;SCo&d&uryDSy?@>1_B3xSrCAj zVQsavYBiluGLvgLfK4dQ!KCffv&N6!v4L@wmA!eNfx&C<*P^w{PI#PcBM6x|qvF$( zP2V_x^bgut+0~j*qzO{ThNm~2!A12(ol-YSml>i*UUgRnVAivPn%3zN-5)Rb*w$2U zpY+X4-%ME8oNM+dh_WY0%mIqOKAT~Ls-&6RaMQRe!~>Z^!|p-UAYIK0#15urAj0S) zzF#ML8(((cEe>SB4R!M#upW?*-c7jU^+%QZ=c9Th;SwbyI66iGjL^7vYdEQFXKXqm zctNJ0TMka8{cZwVfDwc{{jk}3MqM}KyuVv~SGfusl-IY>M#YTpL!KSr^u|mNrU_@o z2l2fb93)_~@&TJ$iHBc_1{HRL)}4f*Xe5It#&eWz+$!dfg7S{Eb`=P$NES zuA(hTu1t9m#w|wV4k93qG;qU%tM6(89(!3q2kb;%t&ZRj+j3X_d(9nOF%6NRArcsi z3;|Wc_YTKVIc`2Qe{67d=^^++NI6Yzf{MgHpLq}ECkSCllgy}?mUYcm%G@8Vmi}30 z0gYA%)ZX0W-_%nBxg-U@KSpfDygtQ3KZE)sS08>NqN!z%OkL|?{@lBn)~^2Q@lu8z z$Bo-Y4*QGcByZ7goF1pLQQSn~TY%n#%VH^Tl|NELP?Th-b<5>VrBODVj%>66^PVe- zU);aPqWU|FSn5%d~=~ zeK2=S1r$&aK;hhE_m8#(x?LOL6g~ZG`1f%v5j4-suzW|oYYQiz0@i`BIisl$1#$F6 zp(nu$k5#6e<&Iu;fQMle9X__ohwc<~2icQToE?=Dbw3|86c%&bAt!?6%oqK9D8V~f zZfAVpU3|C;ySh$W3{*~ye|_3)7Xj+^mQt!%DrIb&VjwL$5%w@>DP)u8<1OqfosP5K zmyD*q>x6_9%QF}G4PapLDW9ll(=ers+);-$rR3~^ zV+=BIR!a0}73-Np4{9NLjbFnA9C z-)ho{-v!3F;ryIY(yfJ0i2-j}i;NVZHZjm%yk*;h?%~ujpLvzc!noNDfl{}>uzQ)l z((qN`HN^Lz?rJ7qk)4{&lDxC)`jG>i$X5!wN#p_;*?kbYhJZI52bG5Qt(Q|=L}AxbiYYd5nv==fhs?5L>{Y?je^kjQx(Mx?7PBC691d^@ozIHvY`$3GZe^ z&eONQUnya}Iv6?hdQW$Yrbf5>){T!h+Dv5uxJa^9%c6hf(%9OiS4B!emS<1)KLp;sUf*)l)exHf!oZ0R6?FeO!b4>6PelR1|QuG&~d3hkyKc! z@P~@;Jv#-bFm}E6%}y1+QRe!y;;Tnklz=E>8J02=##h;wAxRTiQ8B`F9@ddrHSGX6 z0`RMB^7UsA_8u)orCBvYuA&^A!yb6a$v~R3qi3@6k)YhYx8QbAHfBzMJFt7;+mzH= zi&PC-+A78H^oLx#mKulF|%}q zwtD&U-@>PnIGG#~mkg%hH&1;5X*OIb0B9`m^XY|heiD15(ScXV0<#on<~2X4!OLo`;Q22kOID3bBC? z)E+s%GckQs_vdL=kFBMUn!BBqvv!)whCC*y4?xZ2%LF~bxm5FE-U9Zx{(EZ2kH#xn zqL>94_W5A?Wl;VDFwj0!q0_i~|HTUA)Lb|#<8b_D`P;AGUB$^WVLDNzU#uF~*2gd0 zK9)OnX2!LmV-S#pNqdSI{1EQ$WY&~}mL z2bgxD3k66S_P`(OKS1xXJhXV7?EnExiS9zMk;pGhav3kQ8fhN{&hBf$|NV zBDJk1i)1%IIe>syuyWgs7=FKA*$0$OiTEte4pd#a;w{MuxisjA542r(DoV>GIQ~)q<9<}(#vnMeDZssT{i*q`J{jdLRs#2mT zDgYd`*`+}S*c@DBV_Ne*f&<-eH0%&4%Y=xlfVac4&};K3w8|6T5A{LIxZtGuqMDMi zN3z?<4?v5Rd>*c4R;1P6-D8KviQEQ#NiFua%E?dL_LYOh18^=xKCBRh@_I&H8`MqmQ@6Q3l zNCb2{h<^OZ(W!8+v(BLzOZsv(Bw(3IZER|3?IKxcmE7WGN-^a_;lF~h?H$qXeJfvo z#L#juvyifw^MKLN4@>1Wu|^+#Q(g5AAPfYThwRb_jWH~bnrniSyZPgpRB*~A>#}n7 zX_1SZ4X;1{y+Dc1pIO3NqpN$^2-rEvA6|r)^=|sf1$z*rRcn&Y)7O@Hh+qX#w%)Or zRyv~<*?Vg@-(_l*rL#BJ-m=~3NED*+lvVW2O~WK#Nv^%cirGzPH_3bIA=~*n*-QBA z%%#Dacu)vS(AhgfBtJ)3D-EjBBjkR$vy|iYkKH~F`c+B`{lfW;*$a*80;3aPZA<~^ z&%jCKl4l{eEJw$4Mz`I^k2@dxq$4b=Is^A?e2_HfrADK2_|L*sZ?h>p708a3 zZ4i$oWf3kKQvCiqmpLRyG10?niqzR2}rnv>Dr6o4h4#JbjHyH1^B>Q`dvUYRJ;XyFQQJwlk+I67pr~G=A zphD+|01E#?UQST^@^RFgMv;`|{N;Dxqivvt2UbF-Eg;Ju6KPrn)Y}ZM{oWUOQ}X26 z*D~qyrFTu3FZOe;uwA#kD9=1u?Bu;8-?ehp24v|iUqMV^QA~yTu4BMpoYrL0yd)P4 zgO_**Ehf0$Y;_qbK@UQ0HoN)xOfxV&ncQaoU_*aLa(1Z$AR z*5^aCJcnb}jH88Js@fOF1>5*~hxCSJTb&2eYrpg?j*B{T4|4t*)rt?JtP(9LHg5iw zfo)6+BaZQ<+OC!<9CaPGvNYe&o^s|J>@}h(=NaPFzL4AkX3jI%Tm1@#DT*6df=BE& z>)5y4T8sk4&F;yUUCbO$Dh+Xmuic`}0gnB2uHcMI$6CKMx0(^>jXbafkUAUM@cE6| zwr&d(d-?OpzU_(cN#V^#NsRZt0@qk#K1VlY5L>x4ZZ}F>A@ZvqJ>Kx~M!yi|vYu^@ zk&ieVj4dDlYSB(!k!|9zy(z?wkSYDgzABkLTuwG+);|C08^?CGwJg79YlFipvhEo! z=NVyy1C1}=+d2Clw_IX&_cM4YntUzAWOy24o9_z9`E@Dd*wJchfg3k631dquX#NlX zhq$i}tE%hv<<%EOkx~Ic0qK6VlQxCEc+>y4lhVcjA}# zeBV9Kz2~|2-1XOf_Oth#Yt1?47{4*bZ;X}9+{ippqMt5%s39S)@Z5SXQu4L#N7s zevk*%TlXL>NyR!8gVnETphA~JN2#brbQE6ku?X~xA43*ltBB!#*78RCIZ~vQ72dYc z5=LV4_F$i}S?rP1Ojqw??XA!7-GPszm!%MQ*+vgm-js>)kjl*Z(}$ClksVfWPwi#dWrMN(^73hGj1{vjBlFn}nPtUnsHTfX za|Xfg8hKfIZkrwAe~5T_+~m!F-3$@Xtf`3<5TX#K*i@&-OP$IV2$R44XN}(QQ!t(c4NyG9@>uchU zNp?L>>;on#c%;0GTeV;ck7>g!o8 z!8c!p-}-y8nSx^JOI$AlSD@2D}0#WzFcHBm>C zrGZWyhAl$VF+THX+os*nJ;v-#U$*JOOy7#3WMKO1hT);cp~xQ@&@xig@=Lbsi!pa&QfV=Qr=z_)zu{Y>UvDL_!BWM z>2W!>$VF0?vV%82#PZ^LDkz;w=%M4Zt#f`ps(^^gG5cLaQKSG>-^1|MXe!TY89NgaAEO$$7S5E+|GWwAgP=!PdkIV)lAkFus8<0tPF|qv4YAW?) zYEJBc(dlpXSM!rW(X^$c3No^1zO-+9dcyO1AnVp7gN=L%l{a^m&wPv~P#kYQg*%sT zT{a5iXcar$ip%WAEH}+Q5SY6FN}P0DJ*Q4lDG8>Or$HiOb)IG}*RF|AR-)9gZt-Bu zcpcH71c~^kcsLeMFVq~ED~cp0#^;*5@J$L$7X^=CH99CKP9O&}R_)k(H8wuDmRG22 z@EyC>B%02Yj6ZS;zDLU1K6CLE5v4XW5BzNF#M@0%Y%_EV z_EH501FTE2)16D7l}`r7m|31=jm(b_*L0bdbs}WhFzZ;)E@1|%WkAF06)F_>W%sy3 z@hTpxQ|0waXuAW?)q`P{!wPYGug!l}>HP`JYDD;R?MZsNZU&19WkMoK9?dCjiEeEM zMa3%_%oXvoJ z95J6au4L#M(VZ8;)oF5Cb{OoKdtKRxBJ3P=rdCB#eXrjziN%2Ph+kuKi=vV$9sn%#e zP&|c#?o*F-%R1oOvn9xclLa;4$Ho*f;)wit#US>})^R{l+Q7(pj_#Ag^EFkIR?aG_ zHpC@EG|l=Pk&NxDc5xDjPs(7G^_rN5PI=Kh9J@t6J?V@ogF}sQYHs6vDx?8l%nGrN zIXE3y%lKh*)t*H?Ju{W)Gep61HjtDLEt-$N4@A6ohW;q3 z7dUx=rkjO<*dp?oCr@bER4k5}mcxVua8=|l5$U!WC`z=YMejG90(EB0#3P79r2=9c zNq%7_#3EfY&YSTm6(!m#URTUrNbk%9FI4wW0R;amLRTfvK_R^K`ZL!DU!z$Xw=>M? zVaM+UXMM}((8R{75dY%J9g|A3Dv4Tm>7D)K?*$Osy0Is(fzDt)2BUV#95K23)vNIQ z09qus+xXfn50%vjf@Q8xiq6IqU5K{t2h7WZ;luM`J~xG{Rxk5MejSFyu(`9`lf=+$ zYf-(F*!W^2ZB$E}b``!VwxjyzYH0J&>@3N-fV=N96zo?2{)an@*(wEIZJVDq-`r_= z+veye2V_aiK73~TxIUK4(s%}OSul60LAbzQZl6=TuyBs)Ot%BOUge3}E?3)v9fk$I zBn`zT_0BhytBY?mw^{Jjo=Je{3!S{tNa>~-wX2XdV`rEBS#eQN)5DtEBK`_h4s$`y zi+=6UtZmXEKoWdMF5cLbjD@^@n?d{Cg2FEL0eX9))`k&II!+gweY>y0(W z*>QKAul1bzmBLRnjK|mvunK4Ps~jiVE^d$ZFba<=48v}})O+YecW`|%L4Qj2P9x8$ zv`lp=#3-^SX4l>?BwL&0TvL@qvtEsz zVL5Jp&^YKoGtH;(c9r56dVc2L9leM2Z0=vl=He1d$O;>e(r}a?*&vX-R)43@Y6{P$ z-nr^IUn>99?edg2;a7NHaGWq00Wjzl zu}@Mxd9;_&-E595W4rHx*lsB3u&|rMARqaadDjC~YuKQ^SoYWZdA!%V!(D zVZXLv0`-B|*Icyoz}6sP$cR@cFAul7U*n3{5oufzlz2d{$6H16USA@DWPi5tZW5f| z)#etuo`vnK`kMW4FKL&xX!&I79_Fo0K~evDP4rvR1j`tA=#Q_a_wV-KdtZezD!*Ax z15Tt!>)wvkt|C4UaKpA8t(8O)4_c4>Pu@Lebwx#anYjzeI8~=SEuyu;XY`QWk?Q&{Q)1rSC3*$c&*D%9#NNJd2}t;sx|TPsH&y=j=wh3QRZxhJiOf*O zLHYPA?@;5h>fFtD;QDyS@7AN=pHS@5njnzTxVbcGZW^m~mEo4Q;Sl92tzesAF^}nbcesce>WdF@%r; z{|_{J8=T*UKq*0m`A|SE+PkxR)RK4`>g|TthTZ*x9ka{9WZsyPM{C11S;IP3&-;&5 zi8UOmXHP0n)`S^vVGG_o_UVIc*&H;|KlPq{d=wFtgW#W{zao(C*7>(TR(-#Rxa@gy z_ow&iozJOZ58s1dDf<6)NU@tC=R3tAx4-58fie1jC4n5cr3^dY)U*T#e_evWe9Qx` zLuCp{G*XvDM#w_z{WI6%g@hL@vgTUv-LA#Gu+uHjP4;i;RYTiq#0e+hA9oiqxHQPn zNwrAQz;EixT_by2jWj}dCsTV$f&hy>i(SzHrJq;KK7VT36z7(#Is#zF@0w)u*)~uHoNv=E2$@o;N zgb<2-K|HzW&*wiA1Q!FQuHj)#IJhq@0?LJc`4e6ae6S8Rp-ZmTbWUwdzrVg8lHW!1 z>Or{Q+}pyrtNCZ!+j6jLA(_`#0VGB8iv>dwRdgy>>K=yzu;DhMYg)7YBRHrVgd<(e z9O)OJf}w((2Lf_2jTgc_@8Fe!lG3h9Jcruh*b8~;%XjZ~##6aB$aByP>3K!^S@BnT z0=IoPA!=&-!AESe)#3{4U=I{Bv3B89Ol3kIa=MRd<8QB;HVqC$y+o@^B#oo!iUhQ; z_!Mh*39KaZ@bqvaa6c?X4mzv z1tS%2Zt$@6p08*9GHOchl>yT>JXmIpQ|w>qqQG#^z$(?A0yxDcH0YZJ>h&5s(y3VEW!%yG|}6Vx)QIGMk(Ov4tL zuxObt(48EkmdrrX)gJBnI4{H?;;|RDhBE<GQqm#PV zX-(Uw)_C<9JuM%XprNJ=I3>wT{#+Hs{icnG$pwfO(;Auf1Z)Z4Bw}vYqfTqz;=QHt zXN-d`riVd7DiU*9Gpn2?vXg$u8+*5;BfG5wjT92|6g6(1y^AV)VUlw+a!~3?-T1ovHI9gXE6-GZ$W(Pw zCU_PmrlYgDy56(5^(*|U;cdZL$u<_Vu1RjliZsiIJI*x=bhuWKT08ai-B4pJ+1>0m zc)e?9l_sSC6gH1Em-EcHxO4Vu_99(RF}fIbJuKC5df#~;S$BMU0}|oBQZwTX7i(F& zoBBtdjeagS8b!O0>F0~vsc*hu$Cg=hy8?-Vw0BtlIapoUGhcA=`DS4biln)Njl7NP;ZW~>@u-*r?z z75t%*?Lf4Jc~%TfZ-YqgsFv1(`)Zlqru&|+6Bv z9alZo5;f~guO#`++Z8(-}#FuRJbUc2{+*t+^!8N^}t=X_*H3Pdr*naF&LVq4fs{PWw#~g;fWe z;?UA$qn<-Sb%yg-nicck@QyAD5g!8h&lnB%kyDUMSC-{2g2^;elF-BJpz~!`Ygc%N zn1(YH3XYm{)sbVg)3sE^HRav{eq@S+speEqmqW>#EeD%QPzCW*i({gp*^77etAuyN zQaQyt|1M@<46okq zcGOwX$%U!?EO>vEfuyZ#c`+9p5Dpq@3F-?bO53iCay$5#O z25sjmx|angyo4{D-?=Vp+7COx&{%ZRTuyQw5T`aNwo9!N?LAU;=qh)h^`SakQ)Jn4 zM8Ym9@mn*prJM6kM&^2O8`~uR8NtPcS5t1Bmi2}I?r@)JeLLATij0i+h38!&w+$O` zpr|GzhD^&}x@>Yr)i1fOsf;&U)+dR?+FMe~5N3lIfrp$EtMio*nY01aIN$V}z6U=2-Cg`IAlSU#TZ)i!GL`r1N~wwos*hCdTpv&FMA!ht zv-Vim``+DGFHm^D+}t3yjl2b3Y~>ZHH#WKieXt#h?8{P3HEuPW>jzZJ%q=JbUh)Shmvm2cT0G^^ zKSCY=y3H$YR=*A?od65z<|YDch7egL68DwYV?Eq4nm6kxrd0HD=R5c;hngT%B<#!c z>IsUAH?hve-%#PpSO7r162jwGZklX> zMFZF0a3S>Xf#H9OAEj>7Fw+K>I=9wTTBwaXN`T=u2yU>c7?tQYXVpqi6la1A+Pq!# z)?}^Y*bLWrk-B!!wp<*$$9d@7$`AFoK^6d$df&eJ3&c)T0ih0ig$d49NEAyIeLM5Y zB9Zkl(FnrpHFL)WLu1_f4SR9sYs8B(Gb!}iJAA~&b1KcKy^ROcPlLy^i>_CKDA89P zcSZ)e`o-~+8@DdoHyckEdMAgclZKSh21~_*dY_KPNmI=|fD3BQE^b*~agMuH@3rjr z*Fx8`wX41HmKAC$?xl6^wTdkxptIn|DG^3+iy>Oy?n}$d%a3dBr#wlp=s-3;ec(H; zbviee0#z5t%~j2+@*6BT!P|8!)To19JrUy7sufYP2M>8-B`WMAG$x*J*8Fbo>Y_ zk(5+$cAdfbq=Xt*yn6o3Ly@Y{Za&MMA0)q8QfabuHUJCvWfJ)f+w~QbM*sVVB7mH0 z^;3qctz(+Y>QoX!V_8bBHRn`6>!J>G1#J#8+YEb$W)lVKrB-DPBf6GtoM$XyFt)PN zXP6yXF&g@1lV{h*HQpyDHWs`|lrQ2_mRzTnoma1>uP3Cy#=U;^d*jrfpiAG}^zn{> zooYexaND4->j9$Tra` zcHBCV6!n|gAM`_}0KKF-Ik`ObU+$%)V8>@e@<6 zBeo<>IMklk+b1yFqk=Cywh?8uul6HuwnPs$tnv6e{a>B1fyaMw%x;A3-+5;L=>hzQ z1E-sdSaGorKTFQB;}xfv&YH5gVJdqYESsobV*^KY++uXYJnLpBu{~QNgCc2y3SXhO zbxw&gQ+pg_bP^E_BKx?Zb$B`SZtgdw?LjWKwkYT*Z8jPdyNj)2gaqRJi|J8)P#tr%$V#w~zW75S5?bIwgCPB!C(4 z+fi2Lw!!&Z*zn&(2>-pxe{hHZ;`@vMlx7y9`a~y9-H&hfTk!Yc6UDJc z&8~cbVItWrVJ5XvOAglPF!;9)4)mvIZg20z%*oDIb9V$<8A?t5`+uk+gum4vBEa2# zM0@#r)y4I3W2IlM2SN$ZP3#**u~#v#8jrN(e~97^ zMa^C{!f!Y%*#|M)|7rfuopj9A0#jgBIA4H;!U6i{qs#9jgYg>7<&9mWpu>7&X~AZp z{MNgVaa7))Ei#2+r!UgFy#pcKc;4SPKtop4q_p-HNt|%qX2C+ci!%<|WPPjwel?I5 zaSD&;mFwd}!sJq$wKv4_!?53v|ED0vjZyz6nTP&495M9kzVr))D9Xi0met)hRzh&H zAWee2Z8ZXKY>*8lj5MuyYu(qxDO$YScBXon1;)uow(Y%A3$LRm=NO|Kl`6iajiD&# zuLG8;-dE*)I7MC)sJ9DD%!kq^DP&w*l4t8SjH2)M@hqC05BxFKJyigihXB^W z_N>&Yso_FMjJn(Y!G`W^)R&Fx?#&z1# zQ7ye(M}7VFqdj*h2$0E^{^Su$07IVn^f4c1opt4F{hlCg9yj)gWKZx?%@b*~oPz%K zvFb$w!(q1$Pdnw*cv%Ehqo$oLNl-7@iUGId&T()Z8&w~bd-dSqM9-;6WbY|R2(T!h z4iT^H*T&xAkK42wHvFO@BUh(qw-Z{Q9B9U09OMiw7Vls+f;6Ywp_ke5A94xN-Ia)) z5wYcd(b<_>9L7T6;ak!)X4eqI{JLLXypW$P8NZwf7SWDf|7_`W)i8m%?%P;a_Xz(n zn0Pti)OU7}OWUMGHM2^YJLiV{2D0(N^+J_Wu8R#`vn*xzJtlT6zodK1Dr<3cDtOjI z_mc{S!j}RCCQE51Nrt8>SWh624>nivlaj@Zv<-Q3h1`z3kWYFU2N$Xl}hMlS`Z%QW7Y;dLy7^kd^nr&fD$gF(QPVsGS$pyX59{6bV;7z=8hK zWB4UUz> zQ1biG-PcfvrSR9hXC<*w>?cbOV1r#l%CK1>XX`Xk&9Z8CYHiK(Qtqmkmz!UW_N|f+ zRz_VOoNR;?JygNPSCUA`8c+V(xU020Zw^B%xiT78C*di65`U9G8(kI0*wV}M=>;;D z%NeEuTo26kH`3+o_FtgI9qVcle_Or;c3497$ovOWI9K)eK3evK?)xtz@E-FdPWf77 z@t~@mN!t7y_8(#?flaLReo3TuR9C)+oQT!R1#YODUnN-M2rH@IOcGOI9*+rUU)XRg z{3cMP?&ik}7bPKSSO~J#<)CTDE&G18m0vQd)eT#w`&)HGRYKOu9J zvsDdjY%Zrb4ZG|pOxGH1xlzBk_gHntEy`f!wd=TN zoL{Lx-o1{ZpMr3&^eFA7UgOgspaoI8Do~# zsEh_xU6+iZMTb{FnVxXSMb9OC9=K%Bn53wF<1?L+7eDc}xA2~|qfij#n=?m=;Y4}s z!IhsuR#RGZ-cH7ERg-TR9&g-6g-dv)AfMODQTHC0EZ>^`2`?;B&QmcOUKZ%3Z?r{4 zY`pw^;9f;AGQ0m0@1itHSP^ucX?<>icbcVBQb_Ntrnu_x9S|D;M`YtTi?)_k0q`I_ z&P>y>b-`)l3@Ue!U6^K~a63!dOp=qj6~jv_?r~(Pe!>Y1-_PG^e>_OWg?q=(r_1Y1 z3@ahF*d!hB_;{#}7c5sWtSG}8m<&5lE`;c-y$XJRUu?^_ShXKM>p`kB{zox#Xlo!} zgU+rrU;6!Pdi;qkmZ4QEkbI@F?B)vv)U`W*djj9TyG2SzLo2ASNX4ejLf5vHGHiPx zRQW0mgX*i@j2ml+LYNs;d$)1J@Q_MnU_ELw>I2V5rx%bUtHaQzb;lxn4Cu6PZL4wn12OxII?SwPJ! z8WrUC3yOGn=P01aI*i;m*#ekk9GYKC$0DC)e!BkZb(6Ak*y zJz=9ut2rdod3Sb=%f9!e!!Nc!4(?U22QL8{S}%+Oij6O@;*>t{a`=V(&?VQqIVZZE zi`vqj7u?5_CnOO4D4@>sa_&ZTEHq4R)tvj(mGM5w7BZOIt37YRu^I@##VKWKZ$M(R{$b@k9x=z1RL zz@-9ADs9iMoNr?4X9ivF*A?GYv)nqY=NAKYCgZ7+YY=!O>Xoy2wB32sPc5Ama)-0r z_x09S=4Jrp0jlVNSuaeJ;HzlV%~ak7{y*by!9XXqTqAPdkHSk3cQNy^oeC$zVb})g zhhz3I=!v;TxndkQK&DB&|c+7mS`#Zv#Ho~-Kpyy zr{Q4H6;b8gIxE|9gHT9qCqCWwF zd80{%FY{?~BU2?HJ|P*9ZK&efmUgI!2$+M{8Y+r0FX1d(>@D%QECTXvZ3~;j8n*MK zsCEnW=*Ggv+;>E$F3q!-2X%Tt9Dg@A_nJ)&N{|^!(A%1J0I!~@7cd)n>O+nUTpz98 zGEHfFcRD6teYb3sfP7KK9m$I?~#_@PqELUqI34iABG*ZG&5MKg+H^?9ccdpu*!Yc3GB0$v)XudD5y$h1aAN+c#RrQ~_30wVq+*GW zL_i#`y1yN&9vs#CFy?(K4tV5d$pzk>A!)VXF&+(mMVChKJ_!koJu1o=Nb}g&GZ|1n zQvv+;Z=c51t`LLzA>jb4zWX@<$SIm9^1C51F#@gU?bSYwJCH-l;M9AiwoN48eWHz+ zr<#q|>o_6@TgIUkuZQvYp2@yg+zbay*R5e0PpK$%M1j#3Ff*V~kzX(KGUAZZR?zNG z*=rN)y~7wWxmfLYN!uZ;_pF*;Txh{!Llg!AMoln4epK*3Aen*k#a&a&urX3iC?Fs@ zo6%-QfRW2CfpjQxn!E_&KlWlul8vdft6&?W%c6>G?>VJ^c8Y1SjW4zjP$*m3gd{0K#+;I*HhNJAfJ11FovJ!f-<0CgP z?{YaBcPUYaZ%5yQ*MJwX=v6+)ATR45;m9bpD+f48j$3?{N<0llFJPPq{Yb_g%Xooq zkI74(;a6?Rqo6CX(|eKG?n7Zb>uL_sCM)SL;sH|Vyd{AzfI19`6oUTzjhVW3g@s-%NeUGYk_?Fz)d($udK53^aQC3OtWfba6Em{MF`-2LInUsitV8F8&j zK50#gWdQ@1awGHzHRZBwMPl8nq}TyfqK!r?hp(=v+o#*^O+=??dfjxQOeZ$2^f zgcD*%0`mD?)_VL|_Zhb8+oF;oku?=jo0=KOQQk_9$Xc@c^wvq!L|CQjQo1e-?Kcg1 z_>tX0fc+gX53BI7Y`P{Rqdbx*CZ}^@1?YHaLw);Q;zAr5;oGw;nSEU4o_zPNn)~$hU-+)6kKpFrmJ#Qg&)i!OU_T%(dE!s?JweOGSUYQ3H_QiwR#oyA=h zbLV4PuL;U<*G^Vg7}|LjF)CCl=X$QA zb3?BN^+4yAch7$Yg-i8u_PUP|Pl&9sSJ>ycTE5zyG1~6Qp`r`?5M@eD8P#hN_C*jP zH(3z1R-CmqKdTsN^d}3t*QzILrS~8{&$5Q>h7MT)QHEan)!DK=dqfgET?rrh&HepT z^Kh4_r~X@lbE|q+1~Q0%Rqz`4<8-!q#y4;&gZiJ6F6GXi@@Ruc@#HRi_UTtv3N5uM zd~@)w97&lm4tz0$P1N#PN7&d+kUn>+9pS}KrUv-cw0@*pe<2}4TSex+^{x)%xy`Tp z#1Cm+NNRC$Rc|)^_;g=8d^+%I=a~UbhZP6KQ9%94{o8byM*L79n9fq*gpXNUq7Z1{LsVdJ;ect`BC|(Fa1Wk zH=!TsZ&Z|_UzT6YzT``L(CjSQQ{t7e+nOK*=vfY2TBqP=Q~Dr)Id95EferYCP4~>4 zRG1~hm43eU!4{XTD0MdEw;=flOc|3u7*(P*I$#Qi!UqtMa!20_joNlx?$$*` z^oGPQb}rY_DtT1iCmzCk8H#(22sO2m$)>}pf5xljD(ln)$s`YltLt~)Ryuya+b*TT zHyjQvag;@9X)3rWrWmJz<}vvFs>%%B#-q{%u8?`m(RcT|^#F@pd_^Sv_Iob2fm_*Qq5}2l}92);UaE6&42@xr{E5nMM@zo8pVu5yT$0-x==-3It&;vw(2CkRWLF z(W8$2*A}^V&d}b3t{+xO0Kf*wamYkASp(R$(^fJ%!QGvYG|J7 z#XJa%J=VAY5L@7`QSa#h%O!0shuR*8jatoo{#{$&(0=Is$1DT2R`_ku*RMu}#aszb z${RbNWUny8#`OVaau8j1(!-d_)2g9A>NC=NZwfD#oR)Lu5F%*e5Z)u-j*vLmyC)y? zH2<|^v~t_V&-o@tP%{ib7IE>)kulLwRIIVd23ZGUu^T^lhS>UYv6{qf-L9q_I3Oj; zv>a(5x?8GB+V(p#^b6i`{qF#n#JN^{W6KvLGyozB?t2!J$mEq|qTUiblPO)l);v(9 z8lIGs?CzqvlJGxOcbZN#f)o4JsM{w5yJYD%gmX}dHWK?;IH8y$OQz3^-{1!PHx9+j6Qh7!3&@ub`w6QNLzTkvSWJh(8b_A zNN@Eq<8^lCWtZ=|+;kOkcL_J2OB2WaP*mdfDt}f~+Jab$BzmBhbI0Jm^l&*&NiCMs z#qk_aW4=W?_^}VSZT_VoESu}6ncP&llGZaqSLC-k5{GXxcS@Y)-&EuA6O($s4Q&+3 z?KTuW=aaIC&ZL0?L0O@QKa@oqC(*++LnhPJ4a52?#7QDLQBZq0P7gv)^=>*Dyd|0O zz<7LpC580Zc`R~qQ0t1o8_zOIc(|}MB4#i=-E}qT0OBtv6=e%;9$~}+ITbc=%CGs2MA7O~EW{+_)Ln9X{2ogZA$|P-~?pH}Da*%q{{7aQlmZv)dc=05U zrEe?GY`Shmf3-Y|vh&-NwKi@Zu`*8(vpUjz2pja(enTX?ot99*88oxazSu;cF{izJ zP|tp1YE}T-7@f3pmfP<}Y9v&~+$SX_)DJ4@1{HI_izwWKHkGzGtLNGWsT)8l>1Bp8 zqq(;~rN@xwY(7&nEc*2il+;1v3Dj)hkOe2Gv#-y8Bf+D>+IS>j zuG^4(45s~xFfQ)#kOc2vlRuw@He6*SWp-@W7j*l>@^~5OeP{ zH|yr#z!)q@=_PAqXQ;Hdr8F)E5_-f^ZB)r|gDCb|trhX`3a8#4q1o6yCgj9s3qN_goB8;3vev@I5i}z*&isA2+WJ$1 zZc!^s^GWQFk}d+a(D$>&mZdoIZ7cUJjgsulGooq7&hwv37k+mpJv3tZEscNtr3!*- z0&qc%UmsMh^b02{$dulEgz^(OM;y2%)ww#_%_D!> zn;N+olw>rx0x^Ayj>ZXwA;dT*os${ZFGJs`1%WV>3a|IK!QKjc=7m1%Q?7Q#2HAfs z9T4ssA&34s?A09<76l@RJKeAA3nYH68iEnQ_5AIB41>*^rHv=4;x@K{)Y9|xwSI9B zcL71JcW|X%&ckB)oa=;0=C5)G0WZ{%NkF~em^q=J~vw_Wm0)|c8+3qjq~0SL2sI5RE7;{v#}EbqC|wkha{p#6#R z+>LNnw1HBxr%%xbo99%`y#m}M_Pv%brXp1Y2cuUp;`P_!*6neM3(J%JZ6V33CzFmv z$wu+nLKbtCJ@pnREjQ=h*nxM=9I64g>JwIjx*` z6Aju$`~&}>Wfm6T=FvujtVNM)3B~0T41cxgKCj<+njHjiO5r=Qg@N;;<76&ha@3M2 zLfY1DlprzpX#dAkpQn&L1ZsS-7vP`+ES3{cc5bLt*9s#3|GP1<{dOL8yGhiOLMWbA z4;i*SH2V#i>iDDJ6G>%l&x=u37{Px)LxEr;Q7W?L5oGweJxv8noZqyxl-PfEX-Gwu z8*5zfrEvEXMjr0at&ZSW2a^3I7zA%{OmbA|D=mK0XRO$es;pEH3W~ju!dCSMBq!HL zJA8tHypa3P6E?O4g#Iw60DBY}XCxUKjsNz`m%B?#Iky6r85|P>U37(Mb0(Jm#O1~r z5N&D+_(S|p;mrFq{$$U^vA=|iY*48ic|hi9q>~g+IqmnI8}~X``R0&r)aTMi^Kr-@ z;8b`i!Q>yW3$gC+(w<^rg>1|=Z*d9JB?+GrCPSmrPfy;Db>iDxTbEV9HoDwsKTEII z%+ABBc+u}4mcv3l1X^*$#WJmqf3=%EgI94Q-5EQo)XKyv#KUW>YvI!LL8@^A&6V!O ztEGHw$#L;QE5(E0&GYV?6m7XxLJ{uViYbCrMHES9UkU>KB_mW97b>@Gz}rN3`*My< zbX6~^IG*~_bMX+#W=jH${Px$5u4@tEUD;^0eo}@N zR?ys^d2Vo(S^BT|1W&7KOZS4udgWBppl)fC&Xg7*wH@p)R#mmI(X|Od>ETE?6ARPT z%F@098AEqM*mSmsk`uck)9}Vn(2p zIhXVRUV0k4#dN~jwpn<~*<=WaT$R~+b(O`$U=+>S#63gxS{uNHN;Tn0EUL0hJC*s| zO)nW`SFAI1Q7G@p`9k_$$<#`UCRzIC_l40=Ggn{rmP7)cv!dJW)JH#by32FS`HpMn zw2YbN=y&*&6n!Q;PQIB(oyBHGsIHcY#>Ww1MBc!4;sq*=-63v%C z5IP3;R6BmPU;5m6TcVDskCEHSn_v^D&h!CV<_e1}0U_3vh5(+qZMy`SQUk40|4fqc zh(#qB%r1!#a2$+9)Ur(pCTqaz-@&M+T39R&d~+3d6r#8BsFFE?;SBmdub!UAx?D2x zp>XmSwKiOYMS+(i|n8W8x_UUUyVWDN~~ zJGg&tyj$b&_R$llu?r$JfEBM>t`Be)xPe!USDeVM0j`-$6jgeC?xuSPXvDZ1*uAf;jBL!k?o%AG*q`U9KCW!kMgVr6kMn z6&IJtw@^2UAP$fW<)bXL_>$>ypx{=u-^yUvzW%VPgT}UU5HAa}fiArp0W{JdrU$6m zsM3k1s2nm$3Mzj;`A(nsHlzOxiw$yi}fyZ?m&V$b6NA7hLp2*_HD?HZrPB~ z(tIXC?BQ%5NSVnCjFYQcX>2e#(iS7pE*oz)cTv`3^@$>jHMiRp&aYULpRl~DEdaKn zt9YovdW-j`VB`+&7uoIBJBI;J2Iwzhr=Pq7lpY-PCcCZ`3xi&>AW z4%yI-uU<1|{;Rmw$1%0sY43Ohc9*a57&MW&`j_9H*zYH1YB?H3&Uilr(pwC#QN3W) zt_HjoM`!V)>@5G+F*qK?XY*y(+sN-m5e06%$6+DrWx;?t2J=FoaG*hF>ucf3x z3w6`RtNF`m^y)Ys*bk}hzTu}+6b!)Hj}$z(ByHpLMHi^V=NVX9K zH(qMymt|6Z#;C9`iN6+cD{C8&W&#hlEW&pC?PJRRqM+Bp^0-w8j6gSK^IMuBTI^hU z!GYO$2e@!B%h!s_Nk+P>OfPuDCC~l@p_s?WS_u{O}hZeX{i+RGlKih~3D z!a7`qm9tr>uH+OAIF70FQssFPtoFrK8$l!Sl)1Yl8#_K-lu`Nj~fDM^^wl<=-D)BOUj~a=tiO4BptZyJ*Pr!BO$%U-FD3gRd9 z3${{_NtD&0`nX*8v}BNv>RggP)P?;!gF&D0sOY@e07Io1Nz|YplJac5y9jTt?E^_w z$Ghsu$x4H@lxm;`aPM8-8U`$@I1?W>sACFGMs|SRx+ZufpdFV=y}Z=ibfG6hm%6vY z>S}Vi%6VVkeoH{!o*P75KMv2^5gZGp?C0#vZyHf!){ey|7Y6yO#N_0x2=VgUj0U%% zPsa_1xr(>Vv+pE-9?o;0s8J2DO%9oJ?M%?Jq^nYiq1=3Sjs;qAGrGEKM8e#mUC&s~ zoLQ}=^@q0aEOx*S8BfHw1!Rw};?)UOH65Q7Vr|>#K0H}EMeT2h&@PQviE-e1Mn8PC zrJP;gxGxPf-T>Ogw+`FOT!y*L>-vXC*WF3N4kNQ}t(8jT=haZ_9F0B)fZZa{^78ji z)r7`7a360VB=jRyzpXlk`9m6ZmFpb>u$ix5-@P*jQkoxUhHm#3_g9S{gVwev8=;aB zm=>*fjw&+?>F$4(Y|w06t)A&X`ZGcmfbML3Kfdij_WKL7S6c2^VAE4&5u8nDKMTJw z)f)+gtA^yHnqN(FmJ&*Ng~(#zDI!1MGDZ{&9XX8`Kc(%^Qr*1^C8de57G zh50WejOR3etCqx$9AIFskRB9tBE15|UFUZs#@<%}(==Ni8T&e`;YPo|)jW)fR%$?~ z5#h!?0ZNsL9bGyweQDAjhn6P)dlIdv3HT9;pl2rKa7t-NU3ICNbQHakV{Jt|!xt#v z4?vF+sc6y%N0=-;%8dO+ouizLsHiENI+)oj>FWY>Q$OGqIMYl%n=|`^{?rI)hK^Ta zSyaXVQ+CMo&wW06YCSg*|W znYvw$ZA+;B|LE(w25GDUZou11?VNz}C@aMsk-= z^z9w*Th?2fbNYrKqMavwm~TT%7lX5;Sqjpo*(>$44UaRnhJT4La$8UeOWUc-DH zYs&x4T2hE8j-)+Krz@KVO9yLU9Cc!Mdm60|3aO2gw7-NSzH(P820!xHMs%YBoGvRV z1HdPTSHXUY*b79TXVoKzz6IxWK2T*(602-(@)(+3Qh-ip-CXR;uF}!92u%_LZHO?p zJ@qqs-#(r@tZxpe$k8ren8rQr0=lJZw_@DgY8Acij*g1z@Njl)-beS&wA@uTk|bT5 z980w{DM~;~dol0eC%$pb+Lx3_#QqdY2evkoccP+n{g!W+i!7@(RJ%@UTc2}i-EHX{ zuq=Lw=HLQR^gKvO1o2=^B40@Ih(d;U^Km^QBBrc?E z{Qg~sm7$ehe&x5?bMwLeeJ(E7m_OqiT}KsU=f>X;V6>r}{B$y)T0c37sc`NlA{rMZ zy8?-XJ7y^}fomp&!Qd(Kvm*)UZC@E@u{8y{v8$IVW)fsUo6 zZGl3}te+7WPA_;8e+8HBq#zB=JuI)J{>&G5GG)eIl!Jv~E@7+XESNI5XGIv4cQq6T z>q6y^!yW>Xf0x3*-!jf=d!-h+W=yS}g?c-cq=pey7tkRFOF8)Cz#8U@dh_f&;lYI) zn#oI~sq9a~V_-=v{KZyh95Fw;YT#|k`c7*am==Z9^m$xtOGUS#f_?L>U%wjQ_3y60@gLQ^I>|;H>l17G}$j6ctU)$jndl|B%=`1`k;AyR_qL>+# z(l&~ZLQxOTxz5)Rc@6enq|G&}!$$VD3ynd4yEXDZnE6-8w4rhl374-ZGkd>ca84qKFI~^W zMs4+LL~~1NahxBkTwiVpz(vtm85`@1%Jf;&H%Z@yRhZw^o)U5B3bplYpy)OLF2{T! z6)U4qZNqL;<7(S$Ydg+%3IoC%n?GwqeZC!~b1Gn~z0D$TEI&GB;P&1P} z?D?v+6v>1Uo0=kq=dt|3=X=Q>Go2W&HS1pPdi&dZ6Vi0e(Bz#p;G`$j>hdv?m2(G( ziitUTkvi~kkh!`{)*b;%5Ly!Er_U+r@nA~rULjxhp?>mXAlH2w+!%{}%k@Cp1KN{9 z#}=kIvxB`$kSC+?3TlKrND9qSSH3z_JGPd9^$s=SH$EqcD9gJED)5~tOUk3DBME^y zK}y{lHtGs!d-GZBW66Vptx~>cKqDZl72Xa6+>+r=`CN+{^SnL*zV68tcav6gm|a|w zh}A}(>D+S^H-fF~vO7TDius>Dgn3vTN!|A1?)N?46m1xJud8nP zS)qrt$F@d*!K3vNh?jr%QowU?(0YH3OizD$V;(cYI;&fulb^39D9#N898=4+MjrQr zL67CE7k-rWp9d!x)%D$77fy{-F1NIgsYrcO7A>L;nt@$FUmsn zWBP&+XV%9?MaHCa%B6y+V+Z7Tz}f7cv|n%TLL0ZYNb^#$^m>4rrLCrNJIzst(2P|3 z>;ahr1oq8#Be-U`=!ab5R;>q3?=fga5NqZuAEriC9k7uTWhCUmoauS9FRcvaB>Qp}l z@?YlN2%jaMc)}OZGfdq6X_n?+Dfwgf*jqyI9StWi9ZT)!(Yrm;btubQ%vZP(4_*}G zyhML}f<*o6kE!lOF zz*un61FQMr>76Wd1)Ej`7IJa_Y3vu3{R%+77@HyQrO0TJ7V|3bZ}xaO9eTdBUpNum z5qaTOHHL?R{9Ngg5Z(G=aH=hzb#o#=UN%GqVVw7`s|>4?dD8E_#_PBdtpb$tKe~Z2 zmI59K1y0R;IH7O*YOwQdYaDo|w&gS$!|nOB#4-Z>{!8q*TA-o-_3v2uq>}=Yn6Cys zj;t`t%X_IgG6%U5#UOa3h?-p*8luore+i=oK87-LTnu7eZ{%Q0-30s3IJyD_9ntr` z`M%VP`$d6&oiL(ha&J@8o1`Wc5#%P&ohrhYFs*Yg731hWaxQKlaL`j}r(CP(9P-4X z3$*0;+VH!t5k2oz4rOrU3K+p;W_A7=fJtg4NkYA$Kk~eMX=N~51iSF&& z)75#ZC=;*h9lCDOxn!0AQk=g8c=rA;0w|+73*w#QqVK;J{tY>E2JB-NW`Vu>|J?x| zF=8(j+5SGVXEF}iz+-iac2ohTo)K+ujdbz;<{!yFj2;$5xagfS8Kc1w{`PXBvNl^+#x`%)?X^jbZKRe+)!5@W$Y%O6=J7~gA$-rA z+#tL5aJ7X-#;qMXmB8O6|9RkSrRFM`UNle{p~f&;w#xK_)DbIJ7!rLO^-qEkKl(?O zUfwD4JYVe^=8cZA&0Y`r@w^Rz9Nqj=TuBcG%;Ypyy$q#@7X~$q-r}#aj4KUQ|Nai* z_~VqI!(0m9A!r}!q6jon*t)KQab&mi(D?HmbG00L9(BqmMUlORd2dwX{i)V2!l<`I z-?Gir&$~TkcJH zfMf&zg}q9XiPyU`;i8{*9LoRNpE1ON;rrAyC7)Qx0$;$_WT zec`9yh_(xf9f=1A2i?x^mg#?HqhY2v4^E};W9LRgX#3C*G-L-VcTHYGUis*$rC!E{ zMZm68DO0~;({7fkt_;qVG<=eV-UP&CKasT>MxHXcIV_}l&^|)%4r;pcjE0( z;T7wgr|1i8@lf0oRWv>a+r1d~Bhj|a!{)T$u<^|=wh6&ehjR&UszQbfe781cKY(ZJ}i6&Gf! z!xwTX5%pVbC3mIW7(8^!zwE{J)Y3uRccr!hyWy)`7|jjS8)Wkv0cTJtVORQjnC0<- z1Djh`+C67J>PmTOy2ie4OYJS)J**!?*c}ppu&kd=B!}3tE zpWi-COp(s?KkJdf)8bKg`ywC`9W7%@n43mFTyoTz-hcM|3bcR!3UmdUtcUpX57XNv zfU~j{OM5~WA`>>|2{7B)j<$l4ZRu64ew@fwM+yydemb>~#DN71O#ma&)x}|rB-*4m z_}+14nUe@rbS{2_Oe}Wb^cxl*$6w?F2r%l$488; zilO3}N63|YH4FGFNWx4As*NE9+TTL3WQo)^C*Put6i=0@L7{=DfM7@B_7R-Gh@^ma)8cmXgiRt0r%R0`lQ^F*m6EAUrQY%wT2J| zC3O7e6Vn&{rF7IhNTRQjaRGd*m>qbM)i}s5I~)(26_tU)7JU zvd!*ciPIDGk`mubdR!QMg9^W_HVn*)XhObl08V%w+N6 zOOkled8oGXn><~nt(8N9T1T#--RH%3Zp5~-lXiFLPb#G5YAj*MW#Nh<-D&K^&rbk< zUFj=Ds_-JMoJJFR0>)TuPOb6#<*4i|alW?;bcOcE9$b*R+_>oHmxrX`hkVfcbxWf^ z!VCs|xF&Aq%JhVmE(L}&bP#{4X(3>U-x^xuPKY?kQyN@wP$+sm2|(X4E^X44geKbG zBRlw$L2E17%)vg#yQAekZ46U^#A4=ZZ0o~wy2EF8n>9+JSGIy88mQ0uo{Bbo{hlA*lNWlNoKIPV+Lt}Eu~ZT-HO zY?4D5Uk@`0r!M#+48!x1;~CE}FzyJ;U3(@0>flwgBgpbh9tcKzIa@|RHj=OdUmIacE{_!TBt6czQ{-SU2+#m-Vp)+JKU-hYtLpnH&?Qmj zlJWA0J<0LK3O?8fKn8_<@vvBV3G9+gXOcCtD7#TQa8z6IF{AX`ZX3yQ zUEp`$BmY|qQd)>VZ{Lrx+g;7V%!fTJDS<+JEz^~{eljGI%K0XiKkI-hjs?69lu@80 zbxDHf&g=S3Tx-A~YNk-ICgMT6XmLwCpK;BZ$Q_L7+;+raYL#Q#EXJ{geImgt`(8}U za**HHygx##uM%h1kjNg>${uO~+42w8(zgv)-yXKJhwbh0l(ln|ao2r235SV|zVblJ zePG=-Oh2JB|LqG%h&n;I6u5wFzSL`W%C#Vv!a78w+2m4KPuj z16`iQc0m|#)xOQ;#BgH(3<99{j;u>$4dXx*mTr)q%OT(_le14U{H#W*EjR>~#K;S~6N62_fJ2aj zX3%IY-MbpNxiRyT#Uzyj$KSwY7X^FSN6hcY8CwadNHH>0!ZI zO6hWdcIU}t5TIICkzZ99wroG`>LHWU!Usl&sBCMluP7MplIwXPbIxMGj6D-@r1NB2 zVbu?pfMk{I+`f5unKJ8Apm5HJ`9qrHv##TWQ1e)K@Dj6f0AGJsvCT{G>7Zu6%5snM zQw9A)LWE^a!o(huzgua_FynmHCo`>->1Xa`>BQHJX@S2l?a`7N3;T z4r1cBODMu9%rchWiVHH99N&7IF&3XK8UQ*3RQr8 z9+(GavHy}e_MaU1Ad!tb^XZXSr+k|KfZ?`UugA$))Nj&=!0ToxT zw;2QJz9ft8|6=>8E6}FYG@G)+Wg}H(0}L=JdD_G($GEYFvT~CXB_)|W^wXDE=?N?0 z5{JazymJwigh`!&suqe!pKT<7sCe+^rJS%X44~v;NiBF@}|2yZz`NA&(4$ delta 43135 zcmZs?1yodR*FKK=s2G$ANJ}?JHz*+j(%lHs%@C5uKtU9cZicRbK{|#~K%|?Yk%k#M zh8URn9iQiW-~ao)|Btm;i#2mN=RW)1_ul*3*R|93{_5KM>)T-j0CR0*SGyi@HD{~% z1^IP7QgS6yZ9T=)E4SYXD(XG(eDvtoR~f}8Z{KBYMQ*WHhCPCL@8+rRgaCOTN6cp8 zwK$IsLyn`pf_IEhLa}RU2(|Is=kg*eIHLixYMP@Qa|nN&c8SSTya&VS9RY5Cdpo4>}Hh+NYG$`)K>Y&T(k7eZBICta*k z_a#$()`W7*Xl{N{fsgS`?;m#d#q(mz5~h`4^0|!t6C&$EP~#q`o&7JqD+D+pv+sWq z5NHL_Nymuras^L+YvVyABLwGCixY2+XSBMUh^a~0eaL8j1z1+I*-VsgreaGK8u)&peiN8;S+RT2tPJt#?p^-Le@p+z_PIF z0Clm~&Dp*H9GD6T2=khsg=kL~G^fo+d(hkmcgX1uQ>Z0eUW*kspzDRPaVjhFHI$3(6%Vsfr$YnE0EJnu~>hG6IUx#`>$0s?}S z^%lf2L(`fetkQz8!qIYVeN?yn=gbGvz_pJ=v}sKtb>rH7U22lmkDAnX>+i3-M)3wq zp0`X)AcmbQC}1dHQisnBnUlF06Y%rSa8KC%IFU3oa;k3q4zhW4zFO??So7Yr$YF_z z_Ciyji%tp3PM`R=gi7x9_(@CPQN=Ax_h+FHlLWp{2eV#b$m^`CzLPtn&j!`Xf}%rS zli+VM9UCqucpmEJiW=V*(FQ$9|Mcy1g$0eZY5hp3y1FsYB+CpPAPF2T52Bi@sH!Dl zFQCOr4Z3@~?Y;5z5L<{?JxYReS@x?KBSupJccK8tv*h2s6&U14_J|*Sv1@&lonuOg zqtP3ymSs6O)$~gQbR^Mzph}~EPJ~?%lAKD^;MV7C>l9^P#i-suQH!;?La-wL?iK+- z)~Vn|zA*s3(QlqIkL^|Dj}zV_UvO-{I@3Z^SaN-fqjv-n?QdMEhz@H?bor%`^@4_? z`oyT9M3v1ZQz|MmDOHGH2za$u*-s@O9;k{Y&iM63Q)jkZm5_{{1^-ys>U1SuA26bk zbx0Q%-`guLZ7-}+{qS6h#2)9a7n_^Rd|lMdstriZm~d;qPYqtR_p}XK=(1^$|HgAI z%eb)MCO4H`@V%qCi?Qs5LdCABL#{P6vnKUD@c<2{F}B`b*QUadho{e@C;NCJofNLE zaw=R4Q-5Jm(>}fNblLCPHF|$;QT#k+(}9ni$ug?uv-|*PE`-Kh%IDe(sjqQ%3O4h1 zCV}d~^R>cHJ|!r2;@h);G+MqIW7e>fi41ih)~#YhM*5e%KvonMaHkSE`pP1I_(FIL z@87Nvypk3An?izs;LQ@uC7G6jgWn`0CNcXbD;INN;WT-TcM-gejsgBsPhS96I7Ug| z>Pw;ro3D7^#SWDQwliwWqc6Hjzm&MhQ~o?$zK~!XhKO7dTD$w6KcG3S=&q8CT(Pbzf<@pX%Lc;v#`LUKg`NErS_6Yg-Qe9`|NC+1XGqBpr25<&KvER3 z>;dGoWItQoDTS65+v<{e3PF8Y-E62-Ye1D+_J2)<_hETuM9fnz)Kb;8!I$zLYjDup zTl}yHV+)lwSzrm>JB%shRga#2Rbezv6j;hhg=+RgA8uY;k{`RK1@@a{W}HHRL}7c` z^5Rk2z~jK<-mXyEWCl^(B>`aRE>T|0O$mqTSjfG6YU0#b>FB=JVq?sUNSEfIuRpH`W*?!zuIOc8pD|DqQ38?1Yn))uy%ryrO ze`^!R&ad%#`c?55-??gOj&XtodjasEde_LH9WJ1oRG?1#zyXt3yH)4Q5VOIdC8@_p z!?r7(&KVj`I3f}-^v&L45m@Y>SaX z-ST-z$|PC|l8gG;1c)|HY%a52o*EA|B~bZDcJUa+3HUyFqrpx^q|=Tnh_F4LvH!T8 zOg^!m@9YmX^FggRx9jP9;v$;OLI-A8)Ug~6b9~P4;R3mK1yMVcTB0xfFtM()n#J;j zxlf%IewY$^p6X?0AiJ&gpl_lJ^YUK3vb?^G<8d5lS{rkW0_I-Dvk~1gscN5IiL7U( z;jwMUt3PfKen6I^_M%(E@fIUr=cL+)o>U*F9k>KzYWtYWb4c!5&loMjP9fFC&wSD? zN(oZ4F8i23>xai@S^|P6`}rKGUWNM>?(0IQBCX%7{ErzX`0WcN%MmGp!dj5>-vnnB zH!QZXU92qJGXU|cmZ1^s=M+wVk&2Kc%r)B*gCS&5>F%X%PMW!^(cH=|b7UH{fWv9* znOgLE-!Y6zY^xhSJHrwQ$n9Z2Y(~}X&yW}oT2%8cST___;}CCgctviAK^_S;wCO1A zEB#=~2U4hpdIK#?eB`zqkBVAd5hD84Wn<@oBJ*e-VCe#%lpHV1lbzY=!2N(;8=@EB z!jN0vWX(p4xKJydjwM_ZyR}itsn&va_7pd~kAu<1RXIhM&)C`Xi|6NK?c^3h=7UfS zGr+m|M8tNmSAVutRi4BdT-Sa`4Q^Ae^~`?g)2+`VTfRPXeCU11RNZicsQ22t{>){z z#|WkuH#mFQ%+O5_YzAS_1~xFx$Jl;wRwzcc!i{{=)k>AY|dxW=$c4z<<&Ksu+)vKH{GZuw-L{#X*|^gSw7C#(p9 z{QT_}VmYW%K^`J#9*cj;;StT8{UQ05Zm9ZpO82x@e1XLhLx)#31!jhI+EK-9S> zR=6v1q0Zhl(c(DQ|@=8If;f3k%1>yEK_+cchpUNk#+rPoi4=8+Fz{ANSE(|;lT zPefWTeGX*BhRn9TImq{k68_3_%a}csG}kWQq!g7MSDR168|h3u~h0zhH^57W23Xr2E z(`o5=u5GI~o)|iS5|ccG>zqf}-g$jPH>|6PJ8>Ku5g|@a4(uNzu6)080D92bZmik| z!Sgkr$f|>Si)W(=;;TRO1M_`{2cT!I_JoYyl@G-pJrdQX65X7>pL)~#n24l@Sdkp@ zl|`FZ#~qVMQ9AJJTglBRd)GPl;L?zt^{^!3I2$v1_}1)(iT#Rs<6z)fbb6-F`fBoY z23!*7E%B^k1{n8RM6M0NS6Ujnjn_eER1WBv*zrD|@h^V){a`*fCCSE>>^l-uraS(S zz7$5`_!@L%Ql{Plw<&iS&v^rw>Q2~!IBE-5s;L<6c9Q;0SDTEX35Co{+|<(x)$sL8 z6n50eSewjUPS~?DPJ|e)Ob;a(v9|GG&tJLo(7*u%EzDt}xFOKu{*vU2{*S_9k|BdN zf`=xeaH3Ta%+*492#mfrhH?pk?>2 zbzZ>u7r&$txg)19hh@7tHB@vEX_QnFg^&6`T2uObz(AAtdse;*Qp7aYll?wrDIdfT*+o^#m zUvxmE5$61m1Uk?XRE&k#xyR}4hA5$=0wDMj}Mxn#-^1C8PSS~ z>gFw-k{cqezX=*l^0HlZ%a0MfXTmGX0P7$B^nX4CMjz|@2^nEkdW`cy6J1?u0Y#0T zsIV*ZY%z)Oqa6pgskT>ws$l5}ix;Dn&YnBFViTLuwF(AKQ;aEh&1ZuT2rz&grs@_- zV49VyTtxSMZ!%aiG}vB?J6zm>6PedL$zkfs+}W$jqi0K_DhzDf&*v!m0Z9=*WIAbo zXV%nLu*^>=mhkQWct9P<_fJ^IU%m21ioWjG-=?R`+0MAZ-h}L&J;{HJFqa9M#!M^k zQ%zjB9s2Kk?k?0Cdxb=vSLbhX|GbezG-~ecFIf*3I!f+%!!UMnH%l)LaxS^Kv7=Ye zdU>C5Ykf<+_&(4sc3G=VqTa8y4u8>_nl%QucA4ojB^)lONXnh;KCGQ+UjF#vIj%lz zA+eZo)1boJ=4aCak1ygJ^DFOd`Dw_+sGO{EN;8wqnVnj(ma)E)bws6`PWacnio;P~ z`hl@oj?ke}h>6~L^~<-$z5;%>jUu|d_e-RJDcK;eo;{%MV&cVJbv45l%lKMH_e`Jj zw0yd%9|IPZ{3dY%Xyg$Pma^wb(ke?A%-=P=K%e5hQ)&Kx592aLqfrgq80q0YO~`Q^e=`*!yu=k7(koYN!AX3)K%j zg*i+Y{Uc>N&lfKa^gRkE=9RSV_tQyppPsu}{m#C=I5(3fQ(E$xn3O<^<6lV2esK0% zJ&jUx?me2gQO1IlTadzPb?#b)5D9bg4;jhJ)fH^+3o##P6 zSwm~Zx4ZuVtSFq6`xQ!6AoBkS{8EuoF{?++0cVD=qSkx%_?^P))wDyGs{|eh=D#!R zYyy9ov)h)F(bA63e{V?qF#vzve&+eh-^?=BU=zag+O>(TYG0bnp}nWep(Iyv z(za5NVNqA3p?RB>60VdlQ;6t4Xsq{t&{+1{gZS*X?QCr2(=T0*vi1>guS7SI!;A%` zpW|LS0Bs=phvB}7f?>zgvgkA@ z+eC2*RGh;Dzh8Q1_`?o6!SC8OUM~Q2)y}!K%ajbrY~ZmXb9tWL>W^{s{3NVI+;2+O z!=vHz`YNU;T|h z@QO3sNyuA_CdW~9(z0hO{@X0iGfI9|zC0)g(s)B;55OP=nwbLD_%d3ARiXLBt=X=- zXO#$n>oI8*KxB_ny2SWx(%R0y{@E1#Yy*?>azN^vSr~I@k;&86|HMA=hDpLBQ+e+I7@NIwh=4n7L*y8~mPrGWMiA|Bv6Hp4C zS+l50v=^__E6cK!lsx@4xCr|hCR%Lk<<=*n=1*tcKWEOq^4|z?O0mJiJmKK{)}0ZZ z(FFBCp%c1ZCv9Z>Amb?QBZA+)Pya^x^`@2IM{?kt(&AEwz(_;Q$!r1@+xwr_oWb(4 zH^jO9X4NPI+0({nB-EI`J2N6uE*@f*89d+(8SlpqS~MoU819z8JW+k5PrhD0*{ig{ zW8kw;(MQQ}av-V#R%Gq(yuqqN;`U0e{msKKMcxP|FnmU0IrRx&3P0aAeSL?K7}_(! z(_q_LgAE6^Xb`GlJ+|#DV6NjIi)GoLobe29eX(_%)1d8>yXMiPK2jb1ZeqJqbc0&R z$quycgY8-(A)qYhuG4aD?$U<CUJKmR(LUA3S_=WJR(q0e5!}0EN$@R?0*AqQ4S$y z81LGxD&|yibdqKLKswLnc!34pg7LWZz+K_LpitbJ2R+u^MCpO4~>OJAV|@|Hgq@INdDT+n{T8Mc)!OJ{Fy$(8qlW$S08 zJ=pRKw~{C~_LAvHMXUp}bi!$Sg0};s^lc17yC1DWewp1QB&d(vyZblfA8`1(2JbsP zr^Da6qbpS>$UoSSS!x_Vq&M-tkJ2Jy8LxoUP}9WF18x_R>#dt#_m zUHt6?As*DK5$k&Uw*!V4DH;%yG@<%L7*FQQfZ~J`-QAA$3*AKZ;QtRPbwu)f8hVm} z=2`U#H}~@kv%CNO%8lSCKN?A6qtX&Pl`eJPh8r6O?F4VE-vfW$2Qj)Qd+$=TLQ3)G z(q@6hVD%pA^l1~R{7Arhd7RSd^iKw$ccAL-21?4=_$s z13BAZ;sJY@#6q0N|J~;wDEtr_xW}nq75fc#n#ymc$ zJJOuT$oYz*JNzTd&qp@%ej&VVZvI8cNP!DHG0yio$C(FR_IZIJAWHMK>y$gWe07mC z(BsdL@;*9g3{2)Lj8EmtdO#EWUzEl40{`0H1Y|9rhR5^Ml*-%xKZEgyz)&$xYzgR+ z6H*HX%)H?7sweAx^K8ZQmmft*0xxU(OC-|&3sL}oV~TA)M`EGg0=@-nx}#J}k2hxE zKGhZ@&*!L<_-`@#GIT$XZyJ?A91mY!w7*~ZFFdh4yqU`E1?1Xgq}~^_df^2^)F>#w zU4hG#dl^WO%&>5mJXbNiY+~)Ai>cIb01IM9%<;tJ99^oqSv~N7z>vfs;uQ2hw*8Z@ zeq+A9dT-mt-2yRKYw1!a6tH#3$|(}K)pI_V!ruh_-Gp@kdw0=+r>-f~v2NsQVs?Gl z{v|XsFg$}FU%WLyGZ9LPvvUM9L{-w(=ZRN+=OX5WWYD}O89~S20Cx-Y_$XS_z^?q4 zQH!zEAyeqTVTt&9Vk`d)HY8(g*LJ}ji{|T`4pUdfp}iCBx>HZ6rvJr{?B#;|NO^F2 zRtb}UlCc=4;`5QoGsh@)072a4W(dNX#bx>skY=!ms%e!G+?=)j(<^I#Ab%H{E(l!k zwu;|z1Kut14oPBC z(0v{pkv7ie&;7GbIY#QA1EbDnwVgNznPA0lZrCs1(k#GkdmE9~hw$l~i<9kplvlvM zZ$bDHQ$|v>d$up(?b`#bZD(o@o-3t~H(2GW9tWz>9}$87qATzT#Q;Q9+=+L+d51Xf z-u-*G^(8#X2*6bGz!6+dxz#;+7nvd#pWJe=H1(7Id!_gG#0fxpiXVtrF!q#-ll%yo}y3u<5wQmF3_a4*_e5NjEp1mS&nZ;t2k<0Su zEpFfC)i0vs()-fa`ed2QJuJ?`aF|j=j_OF*=+n)&1BYe%rqm?RUMoGa^Oa=7u_pJv_-C^`;ddBu9U@urcy-Zte>ZmEgHM#yH8d^jQyjXf zXkYS2#@oZ|s#hl@RNpa0eg?O(+f$CKoI`T<3T>~^A%!!z)Q5kTdRBUv^AkW zkD81m`yB;F7rRGQ&s8oi8gOrMl^Cr}`xZtixXMKi71|bU%YEx)yUhr61ZMbT&?nst zcDJk%3T!w}?wH{G*vbzZ46%afWddjEbic;GlFI8VnZ-^9J)y!^$q1}A_5cs_%5{}` zYL*v;P}~eNvs5m2iS?wpd}c5nvqEvd!9oqjj(S|<&+*mr^(nxtZ1_z0vpArXu@75% zJ(qn73#uD~y*EQ!r~u`0QBXs6er=q1(Dq~l{f=?_5y>;v-KYxpX=tMx5;wX?OPwgJBposivu#I>kDSl5>rK#3%q*N25|@(UMZXcu&rWDxIf2)<)x)_ zYxn`r5kWjy_qAs&`5H5XpD)W#YCwGFesLpJlHw<0c0 zQo!di+p_|U9ct0N^5JJNa>kBT@$vresb7|c8xL-lZ|Deh^lYoX_4#;Rl|<&z9mY&Y zHmSi<3GSm~GEL$5)(uX-OTA0<7cQ-)>&f|??~mr$D!dTZ-Fh>^u|JQIK2&gesigHq ziwbrB$2!QS+G@vEQu+ALUzvJUJyooc+BMcGTjXe<@{9iBcN3o;V@OaX!ln-<*XH`R zo=_>GKD?e#f{b#p;L&$Ivv_{y8aEcGczKKTYyroQLFH0v7?2OL(KQ@2Y?;oCmQlJ= z#5JaZ`c-w6-tjG?%LkyFm*MybUJ#I?x^cL z%MG|sm(xmiq6=3V-|DXr=B3ZjaV7NPsuwE)yp~a@35A`4%R4;fQ$uGC`~A_rj2(E% zjYb*P))Cdrh9#2}C49&a%ms5LDbm*u_$Ecdy>K*+Y_&9LXnq6ob(&&{uZ+{1^9;Dy z^H!!H|B2`2Tb)uBtl4YuM<(ku1_4-REH7u~- z&9<$#kxhivIPrm!tA!ec{6&j`HqIQ`kXSKFR61Uthj#j3?J-Ik9yS=zEaX=zvw##Sf?Y)m>3luvK(d^niHU?m3y-P)GU-q$xP8K)GYk7 zhpxwUUC+gJ=39)hqwq^S`ofC0&G4Q)%St_lpHYdB@oI1tfH%g6ID$fmWk2VFQye9N zbZNKOX^bL2Sjd>rvC}sJKc!YrW2z({9$}le=)OVQ3P_q?_=AsT$&WaLC4e8zJM9|8 zPG=mX5=nHjs>z320e&XjvsTf#vH?@`y_qkmCn_iLv7%eLezXR0I^zaJ`R;3XoC}_P z=&j{I08x8h2%^pY!yN(f%0n8U2{uMVu>Qy1o&7@pJ@}(>ymm zSViXiq^3Q?nF<=sXXQvb}(3nlH2wQH!+$W>)@H+P*aG!f7+*DB=I z^$ys7#SG?6|6Ku}GlK!6y0>?Rofd{MdyiK4ZydlhRiWNU%P%!_;@NAEda*5iA&~9m zJ=AdHOsn+EE(NDEIITSDCDn~YWsn?$srs5St~F6^Ix81W~7wgA+t<| zX5z9*4qEDowu~SNP6m-30iykt%Q;Zr)J~^cg?r=XVxnWU1-tqqMe#vr|XHChdz0h_<&IAlKu{w1tH?jTFpt$Ey!u%~cnBZ)VZ5cbd+)2O)qzY4pBnqAl)P2!GaLFpwP%FQOe@M*Wm4{|I%ykAMW zRJV=1u5-q3V|X2|Uzn#?arqT>`YFoqB=`4!*dYA#*E^nMZnxo9Tzne1T({|RVHoQm zeE}fnt5K$_BuLfS(FsPk&W9Ba86%6@{@Pd5I=w@=ui>5RS3epK zo0H`X-a*PoF5v1h!2UwgI2CvN&h~9$#aT`;PJ-X=er^oxRUy$&;_ED_GHg8FPfB}# zx%|y*FHi4Kw!za!0$=D=>+%Sj+|dmHdiE?QFN7N0$ctSjeKL^fN~qf@(%7%@lc~$P zr4y7gFC@cTzgNwXVz?lNCsf?qq<`H*YCaxUtKfL}HE7p--(3z*T^*<37=f>T2HMFbdq(R+nG6!Qe5oRn?~Hmg3pQm>^b*AZ%Rl z&aRn5#E*sweLYFVfa7lG?_IlqnV;$Rqt%|}&iyq?-v7Izg32l`1KF zRk85}jc2T6Zp6ML_iUG*GysNSSFdt*6$;x>PS-K?$Tm-r@1x?pX4oXV{CdPC-3>bu zjE=)NY`>dWWY)bMr5s1f)g8ZncA|JqrJQmdUSvbTfr#c94ckzn51ZLv^X-B<1yxdS z@t0jN`_;!eQ)!+y{H|ZIyFAIP+5|WB8v7Rf^2}2|vAk|zEB`j*E29h)nzsQ*{D7fD zi9@GD3Emn0Syk|Fd&vJsG5luvlUohKZtivA8*AqB<37o*j8LDb*tY)TAWYw;yuO-Q z=)DjBu16z2zrGQ-UuXz;9o~N-@CnQ_JX(#LBG>Q&U4p(mn`U77Yx8&cTlG)*EAa2~ zx3PX>jXjw-^MQPLL%UBC#>4c^o%l6k7>o39iyOZBIMJ+NF?ibRs_+)d}#y7zY} z?`W3Sx+%gpV+~Yi%JMun5y7k(5?^!ydW<=>9PkHKpI7tG^Hl!*yqx;y>#NJA zMRf1py*uRhkTs!|;JzEsJ;>8%!Q$tMh)~hPpXz=w&jNGw{tugs<$*tS{#U%@Agcgl zwpPM@%IwhJL2qNhkxsjM2|vA?$1{ZRrVT`CDMOmAYF1% zzNOo7R~}1(7fJ!~rGM#`rHUK{T$b%94Mz}?EYv|@P^kMrWGu^giSe?5&mVmLb2hf^ zr-Sg9^NZ0k8l?C3NW+H76@uTyx-7TYRAL*wcj|-of6$pu@F$C*KP5(!D{-Gayd3`} z*67n;GwdK7a&}gDG8>EpCWz$Z7J{Yf3?fQ7LW>?4djc29?xN+`;KRlR<*0}B6x1jl z%#XuCK4W2^EAdzkFI=~+t0`IMZq&RK7yWTtma>JDz)Lij>eSL|p)0wgmOso5})#DczZhdbR!c62Yw}b{;U)9(}VKLk) zv7w~q2=9>P8hCD3(1{+F_&%Guf1%%?J=76E&+ZqCmZggNPaYC?bavIb%!x8G4j-0) zExVW9m9v;lQ3GeYUS-nrq+?CXYM3Ev(LqcuvwkpeAZVT0z!UiGdi6TDS<(Hye{QP`9rjGM|D;nLTAcG_I4;N)2)C+O>3O zL&HEVY0g3L$w0c~W^_NAfL8kiEHz8SKfFTl#DaRPys!in?n?5EHl;3Xy2P?J4wZrSnPV!RoF+T7T;5alqxNzyhuGk$vxktNmb_+<9+| z9Roub6bwxPM<|3a3JLC&qRcW?doX=21%QfKJvSXM(-eF9Y zopxGY<6Y3iWt*PVK_(7Q8UU|a)BJ;AIw?Q8J=|9@OQ6PON{}sXbEdpyvffb*T|0Q$ zxO7sY&$Z6uB23Nxg(i4qa-TgjVC|5<1%-Ememq8-Tb`W-t*!j%R13pP3W96vPZJdY zQeLQZcV{44LRMA1&jNQa$H#LO&L=eu(^c$DOw!WQefxu!iHaE#*ps%C`SDUwsKqT6 zC;WxlE(sOpSsnZsHPH>!Ha9o7U@>TQ(^}aH)TVoD+T*3Tm-%W5Jra?Qj*ieM+Jt|$ zE;D#-bKH5N+_(<+{bor*>-rAxV`pb|b-ZP6G^C|PIhw%_LV2P<$>6?Q9QTv|!r${S zu`EgP^1L%V+P2}%>7Fl%%YplTuBL37MBseSf+5mvNcTtD+P9mVt~rpPh|0w{zzWlIhwi@rGzGHrL*SXbeu`yhWuMvhT?Z0RL;qq_gck}) zj>B|k29bUTW1Ehfo7{jYIVFQ7!a%(X`V4gA$^H9iQJ16regD0;PV@5?KIk!%<{guF z-F8(|F16U5vK6#*=281t%jE*n<%f3kxKn?F=j`bx@K%ACZO!M;o7Q&_PUA&zu?-+u zR!Bs|XEd>_D+&tq8P`8;Zj_yuU2cV@$Hq3=FW^3WrjR>vK?HFPDrv{>$Y5QjYMU(= z@UBM9)*X8!?d?CnW-BfvCB+!D?LchQ=jIIPR&mMset1lOs;Jv==_lu!vm+m~UQ)*|*0MtgF#Vsww|>=mdqB6COOG*h+-a`f zu{W1DRjkx<_d*VTad{%K-eLf&v4{FOPn5QJ4_{Epz2KrF%bp6&&1@2#CP!!#-XXpf z_32YLS_Av-+mKokzf_eSC7%V4Zlkws0)VJA;*ooiHic=Rv;<&D(AI_O?)!IdD5$EI z3_JYtcTHOMOz7t19NNT7*cw!pa#3(h<;tE2^#PavoGs4HrJa3D87_W zDlIL=!VHWbyRSv@qQy!r7(>72zgupXV*ch?s3bPG7voj$#VRSx!UDBfr1of}x^azQ z587~ct6;fHb8;IaS>r4LcEfbBvuA*guCA+S8mWBw=qe9Gu*llTYHtmV@YUysHUFY3 zeQ_6+EL?^etz{;c&kunDb!xdL$*xOJ`S9OaSzqq`;Z93( z^5oFhp(gmP)}S+0MaBBVa=!#_8XB5Hgqny;XTRb3`xfu;KJ*7>Qo zk=QDhh%Ml`L_DXiq$nRB-X^A3{qFlK1ofPMTI#$q!2?Za?KfI2>wL@F+TJLe2%~X! zb;YChw;AC-JM!}LWiSqREG-v^2W$M&vro3=9)-R}U};)fTF85n@LO@w@(K!G;|&lb zy*tD1gl4rFB=}vi_ofb*q>W&9L~+9oH72gcfN_-CS3yCZ)t*F8S@l`EnPl!2LDmE6T{{msuzDkh^8ihEfedrgY;96Ljm~Zh;6S3y1S>cdAQ6u|5t<^1JteD zx8e2v$SSuXG5#3YP9m~3(vS;+P7Hc|Ur$e}K;TXrZdcf8^3mhRK-Rtj zxL>>5jzV@)auCS*N)?wRm)dbxvw8$K0*HCFo4^(CdU90q-D&K}S_$H-;hqrM% z+ArV)JbKm9b$&C2(_=u1aq!&7$Ocr3v`;``l7)vE=?d4MV8C3V^irO(Y6_%xbVf&l zg8S(*UHL=5`XHCur9{4QZt;9d^Um|~-~2!v64FA1Mg~?ZRr%0}t^z!?>AAnS@3Prl zBbepdvB);(SkTN&O-;oN4?TVP@@0fI9y})^=!rXwTfU$;BF6GKa2>du-)#0AT}z%c z`V(U=7o>KHw(tYHt$Zq)YLAegXLHWNVYV|Z5Fs`LXzMlH>F5sz`y@Vn)IkDw6fZ;Y zf$^?|jGact%KCcX(d4K3-6p^DzA3X~mzkF3qfMc?M&C2LK0o>JiiM!fi_u0RfTplN zS>_vFs@L#hZFVDM!|Sx183J5vBHUXmr?&2d;8=x(s=O6DKHQs20iKiF6ux_LFD1j6y=a``AiLiZ66>!^gVC zGkR85ZRh&jCbe!flnek2dlgjO`}_gU3H&Z*dWzH40T-RbgFeBRWu|j6_+NCSlZv5X z_}U;(Fy!Rt<;|m7K$@-WnWPvY;cVZzaGgmf2fo3Jp7NuMXmZ z;mQkb-|KemtsA7L&OeddID!fU!Vk}RK(Nbxc914e*LVj{ca?^KAfk||^=}s5*E4ON z=_dI7C^^D({R}9XSweyuUiPT!HGH!Hrkin zy#9aP`-9v$4E`7K#tpCFJNn-9 z+bakggGn4id&?%}Z`&TB@m^DMWUSbJSw#CcvCA1@%m}(;tvEYsvNCy)OKW6S<5t1U z9Ji)_*bNkY2(KMDEZQEp5`uQ_*Q4=?l<=Q7u#(wHx=RuZJfwaM*eeBG@(DmMzDi#3 z%5`DS{*jj+H+G1Ju;J1TtWI!h5;u*40(>CkgP!2D((h$fkzi)bl0G&rvKgGRI6qAy z>W%6;G|~7o7Z)gen2ofSOG^|ii3S?Nw+=wzmw^*yF#gaG)JGgytL?%y&sJvVH^!5j zm4#Xv9ss|mBOTk{fzvy%l2?KjGygI%!FANoi1YBYH*&#UIm0v%m=LnsWK%=Sdiq#K zZOTcWW$H8*g9^gNALfK>Awxce)MAgVc8B}U&8?rkP^X%jX|;~JkcURC=`c1dsIJUD zq~_41q869oYPLv921O@dHW^6iy~~0C3um74*Ub3l2n|@+3p)Cfor9M@bc2^+219=S z2T{%UN|IakJstM4UUorKYDylGWc2Bb%SVwu8RjmyRnSF~3d+F$$m@P+KLy9)K9FL8 zyMd)|H|I23?z9ksD&_xDW%lU*&Kzk3haK2#eD79(y9%_6d`TTo{h~AQ*BL0pR_?xS zdjydsItOow+EvI5?f{WJm!50|OVi2c7I35s)MK1K!*2QY`Xx>D}o2Us&Gar{wKVYa4AsGwCsO4;rR0A zTG%nLxiN!cEax5lLCi~tuLa&{v=|7ke-*h2>e*f*EsfR0Oax0&A7v)~(YA(?k})b;D)+lQ}Ow~uuiP(#**VN;mmYW$W zy$yQ;`LJ>XisG+Z{v{#1>}Ae)#`6)NKA}nsrt#V<+~ZWZ3Ld5d`Ce&6S%V@t%zHOz z+;2B}5wt5PgY_GDQ(l*j_ww2DF$~qv|4?#UiL8=mS{PILs)W*JD=JtdN%#K}wVpH% zvTyqy52?)T&hYw_SS0PL)D?Y38ux^5UNv(&2OP$weD_XQ;?}FuO@&RnVU9@fd=u~e zSe~Z=>M0CYq{-JVWXe4{k6*2$nehVLeftQ~A2Td6I>_pvDiP#vD!XyN?ic6oboF}Q zCo7&2rT<&%D<@_0%l{o@j@!^`?@8X`s;Dg8e6(r9_&sQzkto2U85pbC%i*wGGJ9RVoRougto`?R!3wd(clJrQINPsEcVr|ZpVc^O30ea zeuZ=@OL1Do3`+pD3_ZeB`BV%y>1AyLPivA%S~aN-K3XoPK4T2!QHy0UIf(!TY(0pK zAR1|_vHwfaJ5?Bg<=%j9(IA~)jo>Yg8es9@z_*ppv+a_PD(|mJ0W}Bo;(QW#mB-YI zU~ma;QAN!S&n^n=*6#k4nG$RHe(j@ZWVv~II^0rg2T#rqy2AAZfLyrPd zCZ^}eji>FI{x1=BVMIG?=wHbWzU6I>CxR+*AO8Z!I$aN*&BhWXf)m~T{UvKVsa2^E z`iMruR9H@%!dK*N!1HxknP#)o&mAb6NvV?sw<5$W%FTV!)%7A^t7&1BdoG`Q>mDeK zI_)hFO7`G1ND*fFC2`HR-Ih^Nws*V&S=mqFTrerfTP<%qx6@h)j!zzTe$iX67f#Yu z4CfPI#dh$A)<3Y}Cl8pzb4h0C)x5)1oLhuxnkVh=AIBg7+3nzsJYG)TlP*m-RN1jM z)^Ic^{rc}Hy_V{oZdv3~M0(*5GsdszPx^)qIP}cT&*6<_sGr2%N`US-dyyu0GkmCrOW7@n@RLz@5^k(mV zN7UsMp}&`Kpq7FwUcbLdpY@1|hl~Er)d;5F``50J7(5^+rPL->B3-!`lY3KUR9Ic` z5X)>NpH`4RI`kfSxX9omd6=<#cXYSuu$z{l{=GyA;FiL|;ZjG*$BXrh+udqOhge znvxIf&^1a11reEZsXpZ>Y%2=La+_JTtm~w_plSL&HmTXN|4}CMtmxi$G}kryf!4K{ z;LirHXDk~};#O_(0n)9z=Kk5RqD!|=O^yV84qvXbeJ(h?cvi^6E34t}nTVYjPEdXc zwS+%R#DAxxk47dVI^uq73C23llLBpoGcHf$fDO6!udRNylGX|P?v#`bkqPJ?`w+c= z<1;M3wm&2rG33f)x_r5kR@u_*+AKA5Z>y(c%B`*|8Lw~b_BP~TAKO*xDWp0}Z?oEX z(8R=y=FnHXm#(T(>}f|b6Q>*Dw?mXyuV)xkGZzAR>R4fF+1Myut_7_r5^jBtw}}RT z<8oYS;<&6_7V4v3coiqV+)BW4^N37CV|eLlH+Dif^UjU+If;V3rBUmfm?~sJ;7h;2 zXs~TX;lCQ}S7C}@lG;5lYe7lgU$iV6u^=*ctPa$l(X%p+}Yk^kFyEwv(VxyjI zfsfLL9!vUDNSQ?Z5Yd2u`vb#xcli(W8Wje&LW4TVhdL>$qajBUl&wdvO5udbu<| zruqL7_ttSyc3a;#>TLodN+<~EARsN$tk8xX(GydCqgc?|I+P?|DAI>n|N*-YLIqf%h4xT#B4NWXpR!i{|TCJ=L$wr45gjy(-f? z98ZrMMpql_|FEOCBX8%Sd0~~kT7fz&sR&)14)tT@XLtBJ7-RAMf*mddrnmV7i zZ&RfPWB5I`z<{j6fmWa9Y-XXl(nJE%>F49#AojLB>ly0@I@6MgK1xe(FX`#Fa9H%S zOR=`G>{}w7KW^QvlL*g~^-v)UfmkT?``>K&O3v%+^`EHjYitj%MUb5`BF zAX2Ta$pY)YYuKHVi$CIqZW}qh4t4n*)2)rZ-zXJckLzzcSmZX;t>j+rxip4bIijq! zG%g@`izgFL5n+sC$)(b&z6P!K`JLuiX7pL1QIy2tI~IM=`)d+SG#QOqOP<*#KPVse zS*T?-4k9(PEhQG7y`3z2RBWbP%&YYwSAg4cB;i7~m^v3Tb3cQE0<-$>?GtzH#kU&= zcrs0ole3i8)n`i-FUzRH<)evva@d-O1ar{Bt9)y@RlXv3-g9>zyS{;**MGmqu%ZF` zN`@1YAYGmuA#+*bt1k7+g*O(O8cMf|;Wc4u2fQm%Tr2dnHe9$Po_WSJwJ&#O7KsxC zCQe81#A)YW{lP*!9LjZXc&+ld!g6a-cO;+t_oSL3SN0Tr=xgsw70IU3gX7}ukBCvr z-^Q!HHMU?*1^68rB+@unq0fnA?AmEUqT@R%k*f9s+X6!A4YK4(`-*MF6p|a!vXi3O>av5 z3QHy}4Ttaa`6YL)gm3s zVb9u$qd47VIw6}BWzqh2Dd^Y!4`8t29IpY=^u)>llv}_fuv*5A#+PU}mVlA7p z5EjfzoN~(VGtz1dA+w=Fh6(Rc2IZEY1G_VeKwpi=W5^G(rJvJf9_F)s5_5T5CTpM7 zlOQYo_A$Cz$BKX+uks-*IqRvliJqnBN!`q)T|Rt@nYjE!4t^a{Lcs@qO4+NQ#;`+& zTU(FdaALtnk)N=#fv0901KK%z6SbY$c@koEg3PcD;~?!Zh;XB`?ng_z9|Z(W;VY0V zR1of5TO1i7aj5JL?F}#}P9C~8mb-pytPPODx>E`PMvZ z6LsNN6QMvpmJEV={4B8}xey^{k%J&QJ zTzOG*^?Ies;|v5dJ*VhQlxjo*_o3(`LG_mL($mcvvCwDVGDT>!@-JCvm_)`|j_g&Q zwlm(pGftn?#$cwBMG#Wz?wDJ%lNR#sDEYf zec;glvhK9`f)b~t+SvP-A8W$uPd8VOmmj0n3H*W?;L%5!1vUuhLz!I<0k+u&x=l@E z4a;h%)ng#)y>xbFTbK6O1n0Yh?z>hYhwKuN)f=0RX}ve9^_na~HH7!yNw;kEFOtN< ztsAl`9@eLcyT#qEGE(Zh8KbM9otUb!=4ke&N(57)qa~fp0Audy&1+M|XxYwo%vuIp zuW{bYKwz!4TEF2=kLy&Ox^XS&NxTYMl&P$#KhXO+pRM$iSm8In_m3J7YJ*6J?NRL#zX3NDtgQX18GP5w!a>7*8$wm2 zzzVKONS1d*2~Fsi(wCn00Dlzxd8uUCTgUz69ors5WlwIr^_SwQ=TX2_UpdbEKEK<7 zR2v2@R%w^~vW1yDm)$&&81rUI0p?MXSb?orhrY`SwFK{`O_VLiO26o|(=1vmNJZledcI&fa(7Hjexf+`#!V#)RV{?_@kytbT99kcZmZeA)3ug7}nIP_~kRrq+0N|0H$5DqjYka{zr zLeQh0JnPVJ>pHs(33}^_|8cI)%euM=QinHcGOvb5=&aKPTO{do_d@%1Xq zgu~t;g#VNLM7pbqe=1T=j()q@TgQ2PzliD9lOGOi-{*17N3o6q%1>P0PURd^Oq{4t zP6`w`grb+GMjQ{bf$LtgMU9wl4`vTj5hua!1GBu`Ai_%APM%P(VLY*<(~?d35T-Qd zaJkaG#W3jfSS22DOv)6({{S{CG#zAoqX7?+D#O||a+|jRx8*MxJ&@TpdCHDi{0FkQ zg++J3CNulWySYbODzDZzKd=%>mz61j+(l7#$4#j+T~Uv0J8Wdm$?I2ql^Hox##zwZ zD22dJRlfR7fmYUz5IKecN0%v|TFqTpf=Dr~OrrZ_l*~ z<>_d}%C;~o5Ia>h6z#9huS!*|oIq8m?95qubcN8zv7Hb6s?Gzxl6~Jg88BWNYW4Z~ zy0NG2Y@cdRX79VJ{iuc==L8GlMpFK8D0b`o~qMf_DXz|)>aG2gfr$Q zU&H%32;rbgu-v}#P9>C^ww3And3XhMd>5w?U2~|Nph>37C4^Fe>rg>((HW~~jo+Vz z((PMg0%vvA6c5KPK9D9-Gn;$J{JF0LF>_C~!-p_rCvp`ag+ERad(iypbb`IMfb>)4 zzS*Ac3W8P^)JAtUbsVQS{VsNbPKGu^Xjk=TGI23JRo6(ikxQTC(L~vdqwr>xTBQ|W znuP%0@(?z57488z(ljF_mb|G(Autaqp7g zZDKN!ZgV_$pOOj+{kG|*02KA)udGWBxG#~9XOkP8F5iiXa7>VMv5s&Bf!K+0BP|`n z1lo#72bZH=`vOkqph^{ORd_;bPD~COZfjhIOu<9UH{L${zs?K|G&$R1N}GX@c->x|7xnx|MUj?H-m%z zXSVgf=~XuC^rEu7=R?>c^fN3MP{vYj1<-u=v55-tKl{&ZjF!r&wK3KT2HQLdm5l(0 zs`xB|>d4&$gMG&2`$jV`9Gp%kQBG3zB~1P0mW%USMFx+|8OJ5FcN>5)Tl#h@x<*#` zXqo2hl(>Obx?Iw~n4iGZGc|RliG5{|ak*3~K+Mf#25tkC90fE5%$I?-37 zu4((3p{YYSwc+PkQR{VIGZxe-W0a`DzJ|Lq^8q+k?p!$=<+qS`ji9fy=l@oP!ZNGG zaurBtzw{KbG)l(!53FCzgmU68>SL`6UaP;}*Pvd(3t+}sN^Hod8NOSI(|e)(+A|I? zP5Rv4RuPQ{*%p;e>|2Y;)lM}H53VL!;)S@<{(-9Qj?KUP?p|LYbNHK)irLRGVAehi zHiYj?e3Z_I&cZ7P7q!_4To(Vs<_6hZ_6r<2#Tc1r-WM`5N-PksTn^tLTM15PY?HoB z5u3w>O!D*Jo=w~Mi9N5l=*N(o-;$c?pX8Dgey;Hfk-n>m(~R7^>y2pWN1OpL(Y&iJlIZs%1*}+DR3A`&LMpsJZ~eX4I0<5FUImQmQmg zj@W-!e!zAbr+7#gacE#1v_3w6;HtBL#_N;keNM4s^J^1c;S)D5=RB$1LQq3i8nk}Z zc-7JGV5TjGQ(QE)mr+e{X(HTQ1v_7JkhQCwgLy*tyFbIFk*oO^W1nT?V-mTcvf8Mp z3a1W878nYp1$jclr%W`|QbS1Ns?$$GuKAXCZ?EKy=xd$i370+-o%wEF?CIVop^bm! zm_~^_zVW)o<-YBa4z*|=Iwp6y+DDRpTs&4p?TC+VAA zoy&?i7RZ3Yb+&MAYn38AX#lk-)!@I@A1AOgbI-qq|CSeor^rA%{MAODV0#iWrJ;0Q z6vXVVFUg(0{E$#>StUsv5;;zw!j!pevoCTo=IrIOg@td1+xgy{EtX)i=Gr&-q^)YH z<>dEt{Q~xYumN2|9(*KoJg$JM9!xJ) zE$c>RYxCI#5Q&&$(>9TYAk4;5+@)!?LsjX8y71+#(@}lieV*A+;)D6@fK7Q-ux8A= z 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 Q55Bvpt_Lr_areo80f@rlL;wH) From 960e5be4f159f0f32714cd00c54cf67848e091f0 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 21:43:27 +0800 Subject: [PATCH 030/166] Wipe the SSH terminal on clear and reset --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 6 +- docs/updates/2026-09-b.md | 10 +++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_command_widget.py | 40 +++++++-- pybreeze/utils/terminal_text.py | 20 +++++ test/test_utils/test_ssh_screen_clear.py | 88 +++++++++++++++++++ test/test_utils/test_ssh_terminal_output.py | 8 +- 10 files changed, 162 insertions(+), 19 deletions(-) create mode 100644 test/test_utils/test_ssh_screen_clear.py diff --git a/README.md b/README.md index 9a70cb6c..b8bb3a5d 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 1308cb7b..10b5e06e 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index b999a2eb..3cf5c61a 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture_explore.md b/architecture_explore.md index 77dbf011..e3a50c0f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -300,7 +300,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成 `TerminalOutput`:是否先清畫面(`clear`、`reset`:`split_at_screen_clear()`,只留最後一次清除之後的文字,清除前設的顏色延續、`ESC c` 則重設;widget 先 `clear()` 畫面並忘掉待套用的 `\r`),以及一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | @@ -341,7 +341,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 | 套件 | 內容 | |---|---| | `app_dirs.py` | `pybreeze_data_dir()` → `~/.pybreeze`,所有持久化資料的單一位置,建立時為 `0700`(`DATA_DIR_MODE`);`pybreeze_data_path()` 只給路徑、不建立 | -| `terminal_text.py` | 終端輸出的 escape 與控制字元:`strip_terminal_controls()`(CSI、OSC/DCS 等控制字串、nF、兩位元組 escape 剝除,backspace 套用,其餘 C0 丟掉)、`split_incomplete_escape()`(讀取切斷在 escape 中間時把尾巴留給下一次)、`split_unfinished_end()`(再加上它前面或最後的 `\r`)、`take_leading_backspaces()`(一段開頭的 backspace 留給畫面,擦掉前一段已經顯示的字,不越過行首)。SSH terminal 與執行視窗共用 | +| `terminal_text.py` | 終端輸出的 escape 與控制字元:`strip_terminal_controls()`(CSI、OSC/DCS 等控制字串、nF、兩位元組 escape 剝除,backspace 套用,其餘 C0 丟掉)、`split_incomplete_escape()`(讀取切斷在 escape 中間時把尾巴留給下一次)、`split_unfinished_end()`(再加上它前面或最後的 `\r`)、`take_leading_backspaces()`(一段開頭的 backspace 留給畫面,擦掉前一段已經顯示的字,不越過行首)、`split_at_screen_clear()`(最後一個清除整個畫面的序列:`ESC [ 2J`、`ESC [ 3J`、`ESC c`,前後切開;`ESC [ J` 只清游標以下,shell 重畫提示字元時會送,不算)。SSH terminal 與執行視窗共用 | | `terminal_style.py` | SGR(`ESC [ … m`)讀成 `TextStyle`(frozen dataclass:前景、背景、粗體、斜體、底線、反白):`apply_sgr()`(16 色與亮色、`38;5;n` 256 色、`38;2;r;g;b` 24 位元色、各開關與 39/49 預設、0 重設;看不懂或格式錯的參數不改任何東西,超過 5 位數的參數略過,`int()` 不收超過 4300 位數)、`split_styled()`(文字在 SGR 處切段,每段帶它的樣式,其餘 escape 留給 `strip_terminal_controls()`)、`colour_rgb(colour, on_dark=)`(前 16 色用 VS Code 終端機的預設值,深色與淺色主題各一組,`terminal_view.style_format()` 依 view 背景的亮度挑;256 色的色塊、灰階與 24 位元色照 xterm)。只有 SSH terminal 用:執行視窗的程式寫到 pipe,不會上色 | | `subprocess_util.py` | `utf8_subprocess_env()`(釘 `PYTHONIOENCODING`,解 Windows cp950 亂碼)、`no_window_creationflags()`(`CREATE_NO_WINDOW`,避免 GUI 程式彈出黑窗) | | `logging/logger.py` | `pybreeze_logger`(具名 logger,**不動 root logger**)+ `PyBreezeLogger(RotatingFileHandler)`:寫到 `~/.pybreeze/logs/PyBreeze.log`(`PYBREEZE_LOG_FILE` 可改),UTF-8、附加模式、每行帶行程編號,第一筆紀錄才開檔;只在開檔時輪替,門檻 `PYBREEZE_LOG_MAX_BYTES`(預設 100 MB);開不了檔就改寫 `os.devnull` 並警告一次。與 JEditor、FrontEngine 同一套做法(工作區 X-6) | @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 130 個 `test_*.py`、2386 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2400 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 2e04705c..9bacb5c3 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -895,3 +895,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Checked, unchanged**: `tool_json_format.png` and its panel in `tools_montage_a.png`: Format still sorts the keys, so they show what the tool shows. No other screenshot has an arrow label (the HAR tab's hint with one is hidden once a file is open). - **Files**: `images/tool_query_json.png`, `images/tool_url_builder.png`, `images/tools_montage_b.png` - **Open items**: none. + +## U-20260924-299 · 2026-09-24 · clear and reset wipe the SSH terminal · #fix #ssh + +- **What**: `clear` in the SSH terminal did nothing: it sends `ESC [ H ESC [ 2J ESC [ 3J`, which the terminal removed like any other escape, so the old output stayed and the new prompt went under it. `reset` (`ESC c`) did the same. +- **Fix**: `terminal_text.split_at_screen_clear(text)` finds the last erase of the whole screen (`ESC [ 2J`), of the screen and the scrollback (`ESC [ 3J`) or full reset (`ESC c`) and returns what comes before it, the sequence and what comes after. Erasing below the cursor alone (`ESC [ J`, `0J`, `1J`) is not a clear: shells send it to redraw a prompt. `TerminalDecoder.feed()` now returns a `TerminalOutput` (`clears_screen`, `pieces`): with a clear, only what follows is shown, the colours set before it carry on, and a full reset drops them. `SSHCommandWidget._on_data` clears the view first and forgets a rewind still to be applied. A clear cut between two reads is held like any unfinished escape. +- **Tests**: new `test_ssh_screen_clear.py` (14: the three sequences, the last one counting; erasing less than the screen is not a clear; the decoder's output with a clear, a clear cut between reads, the colour carried over or dropped by a reset; the view wiped, a pending rewind forgotten). The first widget test failed on the old code (the second passes there too: the old rewind happened to overwrite the line). The decoder tests in `test_ssh_terminal_output.py` read the text from `TerminalOutput.pieces`. The ad-hoc Hypothesis run of U-20260924-292, with the clear sequences added to its inputs, found nothing. +- **Result / numbers**: 2400 tests in 131 files. Full suite at 8e17ac1: 2371 passed, 15 skipped; both startup tests exit 0; gates clean. `ruff check` clean. +- **Docs**: the three READMEs' SSH paragraph, `architecture_explore.md` (SSH row, `terminal_text.py`, test count). +- **Files**: `pybreeze/utils/terminal_text.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_screen_clear.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6d4c4ff5..ec42c089 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-299 | 2026-09-24 | clear and reset wipe the SSH terminal | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-298 | 2026-09-24 | Arrow labels in the README's tool screenshots | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-297 | 2026-09-24 | README screenshots of the SSH tab, the run window and the diagram editor redone | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-296 | 2026-09-24 | The diff tool shows its diff in colour | #feature #ui | [2026-09-b](2026-09-b.md) | @@ -380,4 +381,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 96 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 97 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 93c36a44..c6e93ccd 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -3,6 +3,7 @@ import codecs import os import weakref +from dataclasses import dataclass import paramiko from PySide6.QtCore import QEvent, QThread, Qt, Signal @@ -27,7 +28,9 @@ from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.utils.terminal_style import PLAIN, TextStyle, split_styled -from pybreeze.utils.terminal_text import split_unfinished_end, strip_terminal_controls, take_leading_backspaces +from pybreeze.utils.terminal_text import ( + FULL_RESET, split_at_screen_clear, split_unfinished_end, strip_terminal_controls, take_leading_backspaces, +) # What closing a channel or a client can raise on a connection already broken CLOSE_ERRORS = (OSError, EOFError, paramiko.SSHException) @@ -54,16 +57,32 @@ def reset(self) -> None: self._pending = "" self._style = PLAIN - def feed(self, data: bytes) -> list[tuple[TextStyle, str]]: - """Return the text *data* completes, in pieces with the style each is shown in. + def feed(self, data: bytes) -> TerminalOutput: + """Return what *data* completes: whether it clears the screen, and the text after. - Escape sequences are removed. Backspaces a piece starts with are kept, - for the view to take back what it already showed; any others are - applied here. + The text comes in pieces with the style each is shown in. Escape + sequences are removed. Backspaces a piece starts with are kept, for the + view to take back what it already showed; any others are applied here. """ text, self._pending = split_unfinished_end(self._pending + self._decoder.decode(data)) + cleared = split_at_screen_clear(text) + if cleared is not None: + before, sequence, text = cleared + # What it wipes is not shown, but the colours it set carry on, + # unless a full reset drops them + _, self._style = split_styled(before, self._style) + if sequence == FULL_RESET: + self._style = PLAIN pieces, self._style = split_styled(text, self._style) - return [(style, _shown(piece)) for style, piece in pieces] + return TerminalOutput(cleared is not None, [(style, _shown(piece)) for style, piece in pieces]) + + +@dataclass(frozen=True) +class TerminalOutput: + """What one read shows: whether the screen is wiped first (``clear``, ``reset``), then its text.""" + + clears_screen: bool + pieces: list[tuple[TextStyle, str]] def _shown(text: str) -> str: @@ -487,8 +506,13 @@ def _start_shell(self, channel: paramiko.Channel, host: str, port: int, user: st host=shown_host, port=port, user=user) + "\n") def _on_data(self, data: bytes): + output = self._decoder.feed(data) + if output.clears_screen: + # `clear` and `reset` wipe the screen; they used to leave it as it was + self.terminal.clear() + self._rewind_pending = False palette = self.terminal.palette() - for style, text in self._decoder.feed(data): + for style, text in output.pieces: self._insert_output(text, style_format(style, palette)) def _on_closed(self, msg: str): diff --git a/pybreeze/utils/terminal_text.py b/pybreeze/utils/terminal_text.py index fa1c9259..911735d1 100644 --- a/pybreeze/utils/terminal_text.py +++ b/pybreeze/utils/terminal_text.py @@ -33,6 +33,13 @@ # Longest such tail held back for the next read; anything longer is shown as is _MAX_PENDING_ESCAPE = 256 +# What wipes the screen: Erase in Display of all of it (2) or of it and the +# scrollback (3), as `clear` sends, and a full reset (RIS), as `reset` sends. +# Erasing below the cursor alone (ESC [ J) is not one: shells send it to +# redraw a prompt. +_SCREEN_CLEAR = re.compile(r'\x1B\[[23]J|\x1Bc') +FULL_RESET = '\x1Bc' + def strip_terminal_controls(text: str) -> str: """*text* without escape sequences, with backspaces applied and other controls dropped.""" @@ -97,3 +104,16 @@ def split_unfinished_end(text: str) -> tuple[str, str]: if shown.endswith("\r"): return shown[:-1], "\r" + held return shown, held + + +def split_at_screen_clear(text: str) -> tuple[str, str, str] | None: + """*text* around its last screen clear: what comes before, the sequence, what comes after. + + ``None`` when it has none. Only what follows the last clear is left on + the screen. + """ + clears = list(_SCREEN_CLEAR.finditer(text)) + if not clears: + return None + last = clears[-1] + return text[:last.start()], last.group(), text[last.end():] diff --git a/test/test_utils/test_ssh_screen_clear.py b/test/test_utils/test_ssh_screen_clear.py new file mode 100644 index 00000000..6ceaf0b5 --- /dev/null +++ b/test/test_utils/test_ssh_screen_clear.py @@ -0,0 +1,88 @@ +"""`clear` and `reset` wipe the SSH terminal, as they do a terminal's screen.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget, TerminalDecoder +from pybreeze.utils.terminal_style import PLAIN, TextStyle +from pybreeze.utils.terminal_text import split_at_screen_clear + +# What `clear` sends with TERM=xterm: home, erase the screen, erase the scrollback +CLEAR = b"\x1b[H\x1b[2J\x1b[3J" + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +class TestSplitAtScreenClear: + @pytest.mark.parametrize("sequence", ["\x1b[2J", "\x1b[3J", "\x1bc"]) + def test_what_follows_the_last_clear_is_what_is_left(self, sequence): + assert split_at_screen_clear(f"old{sequence}mid\x1b[2Jnew") == (f"old{sequence}mid", "\x1b[2J", "new") + assert split_at_screen_clear(f"old{sequence}new") == ("old", sequence, "new") + + @pytest.mark.parametrize("text", ["plain", "\x1b[J prompt redrawn", "\x1b[0J", "\x1b[1J", "\x1b[2K line"]) + def test_erasing_less_than_the_screen_is_not_a_clear(self, text): + # Shells send ESC [ J to redraw a prompt: that must not wipe the view + assert split_at_screen_clear(text) is None + + +class TestTheDecoder: + def test_a_clear_is_reported_with_only_what_follows_it(self): + output = TerminalDecoder().feed(b"old output\r\n" + CLEAR + b"$ ") + + assert output.clears_screen + assert output.pieces == [(PLAIN, "$ ")] + + def test_a_clear_cut_between_reads_is_still_one(self): + decoder = TerminalDecoder() + + first = decoder.feed(b"old\r\n\x1b[H\x1b[2") + second = decoder.feed(b"J\x1b[3J$ ") + + assert not first.clears_screen + assert second.clears_screen + assert "".join(text for _style, text in second.pieces) == "$ " + + def test_a_colour_set_before_the_clear_carries_on(self): + output = TerminalDecoder().feed(b"\x1b[31mred" + CLEAR + b"still red") + + assert output.pieces == [(TextStyle(foreground=1), "still red")] + + def test_a_full_reset_drops_the_colour(self): + output = TerminalDecoder().feed(b"\x1b[31mred\x1bcplain") + + assert output.clears_screen + assert output.pieces == [(PLAIN, "plain")] + + +class TestTheTerminal: + def test_clear_wipes_the_view(self, app): + # The sequences were removed and nothing else happened: `clear` left + # everything on the screen + widget = SSHCommandWidget() + widget._on_data(b"line 1\r\nline 2\r\n$ clear\r\n") + + widget._on_data(CLEAR + b"$ ") + + assert widget.terminal.toPlainText() == "$ " + widget.close() + + def test_a_rewind_left_before_the_clear_is_forgotten(self, app): + widget = SSHCommandWidget() + widget._on_data(b"50%\r\x1b[32m") # ends on a rewind still to be applied + + widget._on_data(CLEAR + b"$ ") + + assert widget.terminal.toPlainText() == "$ " + assert not widget._rewind_pending + widget.close() diff --git a/test/test_utils/test_ssh_terminal_output.py b/test/test_utils/test_ssh_terminal_output.py index bde6ed9a..a80eb1f1 100644 --- a/test/test_utils/test_ssh_terminal_output.py +++ b/test/test_utils/test_ssh_terminal_output.py @@ -31,9 +31,9 @@ def app(): return instance -def _text(pieces) -> str: - """What the decoder's pieces show, their styles left out.""" - return "".join(text for _style, text in pieces) +def _text(output) -> str: + """What the decoder's output shows, its styles left out.""" + return "".join(text for _style, text in output.pieces) class TestTerminalDecoder: @@ -521,7 +521,7 @@ def test_a_new_session_starts_without_the_last_one_colour(self, app): decoder.reset() - assert decoder.feed(b"plain") == [(PLAIN, "plain")] + assert decoder.feed(b"plain").pieces == [(PLAIN, "plain")] @pytest.mark.parametrize("chunks", [ [b"50%\r\x1b[32m60%"], From c8741d699e4e96e421577e5f52a52c3ba7976f1b Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 21:49:41 +0800 Subject: [PATCH 031/166] Correct the record on the automation keyword colours --- docs/updates/2026-09-b.md | 7 +++++++ docs/updates/README.md | 3 ++- progress.md | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 9bacb5c3..8875a617 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -905,3 +905,10 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: the three READMEs' SSH paragraph, `architecture_explore.md` (SSH row, `terminal_text.py`, test count). - **Files**: `pybreeze/utils/terminal_text.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `test/test_utils/test_ssh_screen_clear.py`, `test/test_utils/test_ssh_terminal_output.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-300 · 2026-09-24 · Correction to U-20260924-295: the keyword colours are not shown yet · #docs #jeditor + +- **What**: U-20260924-295 said the automation keywords showed in fixed yellow that could not be read on a light theme. They are not shown at all today: JEditor has built-in rules for `.json`, `.yml` and `.yaml`, so the editor gets its `GenericHighlighter`, which does not read the registered words (progress #103, blocked on JEditor). U-295's test built JEditor's `PythonHighlighter` directly, which is the one that reads them, so it passed without showing what the IDE does. +- **What stands**: the registered colours are theme colour keys, the form JEditor's `GenericHighlighter` uses for its own rules (`_format_for(colour_key)`, `je_editor/pyside_ui/code/syntax/generic_syntax.py:48` in `D:\Codes\JEDITOR` at 4076fab), so when #103 is fixed the keywords follow the theme without a change here. #103 now says the words' colour should be taken as a key. +- **Files**: `progress.md` +- **Open items**: #103 (unchanged, blocked). diff --git a/docs/updates/README.md b/docs/updates/README.md index ec42c089..23c2f3e4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-300 | 2026-09-24 | Correction to U-20260924-295: the keyword colours are not shown yet | #docs #jeditor | [2026-09-b](2026-09-b.md) | | U-20260924-299 | 2026-09-24 | clear and reset wipe the SSH terminal | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-298 | 2026-09-24 | Arrow labels in the README's tool screenshots | #docs | [2026-09-b](2026-09-b.md) | | U-20260924-297 | 2026-09-24 | README screenshots of the SSH tab, the run window and the diagram editor redone | #docs | [2026-09-b](2026-09-b.md) | @@ -381,4 +382,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 97 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 98 | diff --git a/progress.md b/progress.md index 3e321cea..a2d897db 100644 --- a/progress.md +++ b/progress.md @@ -14,6 +14,6 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#89** [DECIDE] The prthinker settings offer the Gemini, Cohere and Mistral backends but have no field for their keys: prthinker reads `PRTHINKER_GEMINI_API_KEY` / `_COHERE_` / `_MISTRAL_` from the environment the IDE was started in (as `test_prthinker_contract.py` notes), so choosing one of them in the dialog alone always fails for want of a key (`dialog/prthinker_setting_dialog.py`, `extend/prthinker_extend/prthinker_setting.py`). Should they get password fields like the OpenAI and Anthropic keys, or the dialog say where the key comes from? Related: a saved backend, platform or RAG value the dialog does not list is shown as the first item and replaced on the next Save, on purpose (`test_a_stored_value_that_is_not_on_offer_leaves_the_first_choice`); a newer prthinker's value is lost that way. Keep it, or list the unknown value? - **#92** [DECIDE] The embedded JupyterLab has no token (`--ServerApp.token=`): the loopback bind keeps browsers and other machines out, but not other accounts on a shared machine (RDS, a lab Linux box), which get a kernel as the IDE user. Generate a token per launch and load the view with it? The Security › JupyterLab rule would change with it. - **#102** [BLOCKED] A report mail can wait forever: je_mail_thunder's `SMTPWrapper(host, port)` passes no timeout to `SMTP_SSL`, so a mail server that accepts the connection and then stalls holds the report-mail thread (`extend/mail_thunder_extend/mail_thunder_setting.py`, `send_report`) with no end, and the run window never says whether the report went. Needs a `timeout` argument in MailThunder (`je_mail_thunder/smtp/smtp_wrapper.py:20`); then pass 30 s here. -- **#103** [BLOCKED] PyBreeze's automation keywords are never highlighted: `syntax_extend_package` registers them for `.json`, `.yml` and `.yaml` (`pybreeze_ui/syntax/syntax_extend.py`), but JEditor has built-in rules for those suffixes, so `CodeEditor.reset_highlighter` gets a `GenericHighlighter` from `highlighter_for`, and only `PythonHighlighter` reads the plugin registry (`je_editor/pyside_ui/code/syntax/generic_syntax.py:124`, `code_edit_plaintext.py:423-425` in `D:\Codes\JEDITOR` at d372c35). Needs JEditor's generic highlighter to add a registered language's words for the suffix; the `architecture.md` §6 contract changes with it. +- **#103** [BLOCKED] PyBreeze's automation keywords are never highlighted: `syntax_extend_package` registers them for `.json`, `.yml` and `.yaml` (`pybreeze_ui/syntax/syntax_extend.py`), but JEditor has built-in rules for those suffixes, so `CodeEditor.reset_highlighter` gets a `GenericHighlighter` from `highlighter_for`, and only `PythonHighlighter` reads the plugin registry (`je_editor/pyside_ui/code/syntax/generic_syntax.py:124`, `code_edit_plaintext.py:423-425` in `D:\Codes\JEDITOR` at d372c35). Needs JEditor's generic highlighter to add a registered language's words for the suffix, taking the colour as a theme colour key as its own rules do (PyBreeze registers `warning_output_color` and `diff_modified_marker_color`); the `architecture.md` §6 contract changes with it. - **#106** [DECIDE] `test_pioneer` is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), and before 0.1.34 it read a YAML file in the locale's encoding: on a Traditional Chinese Windows (cp950) TestPioneer "Run YAML" of a file with a non-ASCII character, which the editor saves as UTF-8, exits 1 with `UnicodeDecodeError` (`extend/process_executor/test_pioneer/test_pioneer_process_manager.py`). This repo's `.venv` has 0.1.30; 0.1.34 reads it as UTF-8. Should the dependencies require `test_pioneer>=0.1.34`? - **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. From a89cb68b54e2dbbe2486fed97d3879dcea55f2c2 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:02:33 +0800 Subject: [PATCH 032/166] Build the automation Install menu from a table --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 8 ++ docs/updates/README.md | 3 +- .../build_automation_install_menu.py | 99 +++++-------------- test/test_utils/test_install_menu.py | 40 ++++++++ 5 files changed, 75 insertions(+), 77 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index e3a50c0f..644a2782 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2400 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2401 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 8875a617..e87a4497 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -912,3 +912,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **What stands**: the registered colours are theme colour keys, the form JEditor's `GenericHighlighter` uses for its own rules (`_format_for(colour_key)`, `je_editor/pyside_ui/code/syntax/generic_syntax.py:48` in `D:\Codes\JEDITOR` at 4076fab), so when #103 is fixed the keywords follow the theme without a change here. #103 now says the words' colour should be taken as a key. - **Files**: `progress.md` - **Open items**: #103 (unchanged, blocked). + +## U-20260924-301 · 2026-09-24 · Build the automation Install menu from a table · #refactor #menu + +- **What** (refactor, no change in behaviour): `build_automation_install_menu` repeated the same six lines for each PyPI package, and kept every action on the main window under an attribute nothing read. Its entries now come from `PYPI_PACKAGES` (label key, package), each action parented to the submenu so it is kept alive, followed by prthinker's. The six one-line `install_` functions are gone; nothing called them. +- **Tests**: new `TestTheAutomationInstallMenu` in `test_install_menu.py` builds the menu and triggers each entry: labels in order and the package each one installs, prthinker last. Written first and passing on the old code. +- **Result / numbers**: 2401 tests in 131 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py`, `test/test_utils/test_install_menu.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 23c2f3e4..a40fc543 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-301 | 2026-09-24 | Build the automation Install menu from a table | #refactor #menu | [2026-09-b](2026-09-b.md) | | U-20260924-300 | 2026-09-24 | Correction to U-20260924-295: the keyword colours are not shown yet | #docs #jeditor | [2026-09-b](2026-09-b.md) | | U-20260924-299 | 2026-09-24 | clear and reset wipe the SSH terminal | #fix #ssh | [2026-09-b](2026-09-b.md) | | U-20260924-298 | 2026-09-24 | Arrow labels in the README's tool screenshots | #docs | [2026-09-b](2026-09-b.md) | @@ -382,4 +383,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 98 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 99 | diff --git a/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py b/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py index 05ea8358..412469e7 100644 --- a/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py +++ b/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py @@ -15,83 +15,32 @@ if TYPE_CHECKING: from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow - -def build_automation_install_menu(ui_we_want_to_set: PyBreezeMainWindow): - ui_we_want_to_set.install_automation_menu = ui_we_want_to_set.install_menu.addMenu( - language_wrapper.language_word_dict.get("automation_menu_label")) - # Try to install AutoControl - ui_we_want_to_set.install_autocontrol_action = QAction( - language_wrapper.language_word_dict.get("install_menu_autocontrol")) - ui_we_want_to_set.install_autocontrol_action.triggered.connect( - lambda: install_autocontrol(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_autocontrol_action) - # Try to install APITestka - ui_we_want_to_set.install_api_testka = QAction( - language_wrapper.language_word_dict.get("install_menu_apitestka")) - ui_we_want_to_set.install_api_testka.triggered.connect( - lambda: install_api_testka(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_api_testka) - # Try to install LoadDensity - ui_we_want_to_set.install_load_density_action = QAction( - language_wrapper.language_word_dict.get("install_menu_loaddensity")) - ui_we_want_to_set.install_load_density_action.triggered.connect( - lambda: install_load_density(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_load_density_action) - # Try to install WebRunner - ui_we_want_to_set.install_web_runner_action = QAction( - language_wrapper.language_word_dict.get("install_menu_webrunner")) - ui_we_want_to_set.install_web_runner_action.triggered.connect( - lambda: install_web_runner(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_web_runner_action) - # Try to install Automation File - ui_we_want_to_set.install_automation_file_action = QAction( - language_wrapper.language_word_dict.get("install_menu_automation_file")) - ui_we_want_to_set.install_automation_file_action.triggered.connect( - lambda: install_automation_file(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_automation_file_action) - # Try to install MailThunder - ui_we_want_to_set.install_mail_thunder_action = QAction( - language_wrapper.language_word_dict.get("install_menu_mail_thunder")) - ui_we_want_to_set.install_mail_thunder_action.triggered.connect( - lambda: install_mail_thunder_file(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_mail_thunder_action) - # Try to install prthinker - ui_we_want_to_set.install_prthinker_action = QAction( - language_wrapper.language_word_dict.get("install_menu_prthinker")) - ui_we_want_to_set.install_prthinker_action.triggered.connect( - lambda: install_prthinker(ui_we_want_to_set) - ) - ui_we_want_to_set.install_automation_menu.addAction(ui_we_want_to_set.install_prthinker_action) - - -def install_autocontrol(ui_we_want_to_set: PyBreezeMainWindow) -> None: - install_package("je_auto_control", ui_we_want_to_set) - - -def install_api_testka(ui_we_want_to_set: PyBreezeMainWindow) -> None: - install_package("je_api_testka", ui_we_want_to_set) - - -def install_load_density(ui_we_want_to_set: PyBreezeMainWindow) -> None: - install_package("je_load_density", ui_we_want_to_set) - - -def install_web_runner(ui_we_want_to_set: PyBreezeMainWindow) -> None: - install_package("je_web_runner", ui_we_want_to_set) - - -def install_automation_file(ui_we_want_to_set: PyBreezeMainWindow) -> None: - install_package("automation_file", ui_we_want_to_set) +# The entries installed from PyPI, in menu order: the language key of each +# label and the package it installs +PYPI_PACKAGES: tuple[tuple[str, str], ...] = ( + ("install_menu_autocontrol", "je_auto_control"), + ("install_menu_apitestka", "je_api_testka"), + ("install_menu_loaddensity", "je_load_density"), + ("install_menu_webrunner", "je_web_runner"), + ("install_menu_automation_file", "automation_file"), + ("install_menu_mail_thunder", "je_mail_thunder"), +) -def install_mail_thunder_file(ui_we_want_to_set: PyBreezeMainWindow) -> None: - install_package("je_mail_thunder", ui_we_want_to_set) +def build_automation_install_menu(ui_we_want_to_set: PyBreezeMainWindow): + """Add the Automation submenu of Install: one entry per package, then prthinker.""" + words = language_wrapper.language_word_dict + menu = ui_we_want_to_set.install_menu.addMenu(words.get("automation_menu_label")) + ui_we_want_to_set.install_automation_menu = menu + for label_key, package in PYPI_PACKAGES: + # The menu is the action's parent: a menu does not own what is added to it + action = QAction(words.get(label_key), menu) + action.triggered.connect( + lambda _checked=False, name=package: install_package(name, ui_we_want_to_set)) + menu.addAction(action) + prthinker_action = QAction(words.get("install_menu_prthinker"), menu) + prthinker_action.triggered.connect(lambda: install_prthinker(ui_we_want_to_set)) + menu.addAction(prthinker_action) def install_prthinker(ui_we_want_to_set: PyBreezeMainWindow) -> None: diff --git a/test/test_utils/test_install_menu.py b/test/test_utils/test_install_menu.py index f60a7762..6b1538d6 100644 --- a/test/test_utils/test_install_menu.py +++ b/test/test_utils/test_install_menu.py @@ -108,6 +108,46 @@ def test_one_pip_installs_all_three(self, app, processes): assert _runs(processes) == [("pip", ["install", "-U", "setuptools", "build", "wheel"])] +class TestTheAutomationInstallMenu: + """Each entry installs its own package.""" + + @staticmethod + def _menu(app): + from types import SimpleNamespace + + from PySide6.QtWidgets import QMenu + + from pybreeze.pybreeze_ui.menu.install_menu.automation_menu.build_automation_install_menu import ( + build_automation_install_menu, + ) + + window = SimpleNamespace(install_menu=QMenu()) + build_automation_install_menu(window) + return window, window.install_automation_menu + + def test_the_entries_and_their_packages(self, app, processes): + from je_editor import language_wrapper + + window, menu = self._menu(app) + words = language_wrapper.language_word_dict + pip_entries = [action for action in menu.actions() + if action.text() != words.get("install_menu_prthinker")] + installed = [] + for action in pip_entries: + action.trigger() + installed.append((action.text(), _runs(processes)[-1][1][-1])) + + assert installed == [ + (words.get("install_menu_autocontrol"), "je_auto_control"), + (words.get("install_menu_apitestka"), "je_api_testka"), + (words.get("install_menu_loaddensity"), "je_load_density"), + (words.get("install_menu_webrunner"), "je_web_runner"), + (words.get("install_menu_automation_file"), "automation_file"), + (words.get("install_menu_mail_thunder"), "je_mail_thunder"), + ] + assert menu.actions()[-1].text() == words.get("install_menu_prthinker") + + class TestInstallingPrthinker: def test_a_remembered_source_folder_is_installed(self, app, processes, monkeypatch, tmp_path): from pybreeze.pybreeze_ui.menu.install_menu.automation_menu import ( From 176274c90d774457ebc953679f7520c5e2d42875 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:06:26 +0800 Subject: [PATCH 033/166] Install TestPioneer from the Install menu --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/menu_install.png | Bin 14272 -> 7863 bytes progress.md | 2 +- .../extend_multi_language/extend_english.py | 1 + .../extend_traditional_chinese.py | 1 + .../build_automation_install_menu.py | 1 + test/test_utils/test_install_menu.py | 1 + 12 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b8bb3a5d..4150699e 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ Once launched: | **WebRunner** | Browser driver integration, element location and interaction, web test scripting, reports | | **LoadDensity** | Concurrent request simulation, performance metrics, stress scenario management, reports | | **MailThunder** | SMTP sending, HTML report delivery, attachments, environment-variable configuration | -| **TestPioneer** | YAML test definitions, template generation, structured execution | +| **TestPioneer** | YAML test definitions, template generation, structured execution; `Install ▸ Automation ▸ Install TestPioneer` installs or upgrades it (0.1.34 and later read a YAML file as UTF-8 whatever the system locale) | | **File Automation** | Automated file and directory operations, batch processing | | **prthinker** | Chain-of-thought code review of a file or a Pull Request; settings in `~/.pybreeze/prthinker_setting.json`; installed from its own source folder via `Install ▸ Automation ▸ Install prthinker` (needs Python 3.12+) | diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 10b5e06e..c924383e 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -344,7 +344,7 @@ start_editor(theme="dark_teal.xml") # 任意 qt_material 主题 | **WebRunner** | 浏览器驱动集成、元素定位与交互、Web 测试脚本、报告 | | **LoadDensity** | 并发请求模拟、性能指标、压力场景管理、报告 | | **MailThunder** | SMTP 发送、HTML 报告投递、附件、环境变量配置 | -| **TestPioneer** | YAML 测试定义、模板生成、结构化执行 | +| **TestPioneer** | YAML 测试定义、模板生成、结构化执行;`Install ▸ Automation ▸ Install TestPioneer` 可安装或升级(0.1.34 起无论系统区域设置,都以 UTF-8 读取 YAML 文件) | | **File Automation** | 自动化文件与目录操作、批量处理 | | **prthinker** | 对文件或 Pull Request 进行思维链代码审查;设置保存在 `~/.pybreeze/prthinker_setting.json`;通过 `Install ▸ Automation ▸ Install prthinker` 从它自己的源码文件夹安装(需要 Python 3.12+) | diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 3cf5c61a..a85edeb6 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -344,7 +344,7 @@ start_editor(theme="dark_teal.xml") # 任何 qt_material 主題 | **WebRunner** | 瀏覽器驅動程式整合、元素定位與互動、Web 測試腳本、報告 | | **LoadDensity** | 併發請求模擬、效能指標、壓力情境管理、報告 | | **MailThunder** | SMTP 寄信、HTML 報告寄送、附件、以環境變數設定 | -| **TestPioneer** | YAML 測試定義、範本產生、結構化執行 | +| **TestPioneer** | YAML 測試定義、範本產生、結構化執行;`Install ▸ Automation ▸ Install TestPioneer` 可安裝或升級(0.1.34 起不論系統語系,都以 UTF-8 讀取 YAML 檔) | | **File Automation** | 自動化檔案與目錄操作、批次處理 | | **prthinker** | 對檔案或 Pull Request 進行思維鏈程式碼審查;設定存於 `~/.pybreeze/prthinker_setting.json`;透過 `Install ▸ Automation ▸ Install prthinker` 從它自己的原始碼資料夾安裝(需要 Python 3.12 以上) | diff --git a/architecture_explore.md b/architecture_explore.md index 644a2782..1cacbc5d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -207,7 +207,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) `install_utils.install_packages()` 用 `build_task_process()` 開一個執行視窗,`start_module_process("pip", ["install", "-U", *packages])`:參數清單、不經 shell(以前借 JEditor 的 `ShellManager`,它用 `shell=True` 交給 `cmd.exe`,使用者選的資料夾名稱裡有 `&` 就會把指令切開)。多個套件一次 pip(建置工具以前是三個 pip 同時對同一個環境跑)。pip 用 IDE 選定的直譯器,沒選時照一般執行的退路。`install_package()` 是單一套件的寫法 -- `automation_menu/` — 七個自動化套件的一鍵安裝。**prthinker 例外**:不在 PyPI 上,第一次會問來源資料夾、記進設定,之後裝 `[runner]` +- `automation_menu/` — 八個自動化套件的一鍵安裝(PyPI 上的七個列在 `PYPI_PACKAGES`)。**prthinker 例外**:不在 PyPI 上,第一次會問來源資料夾、記進設定,之後裝 `[runner]` - `tools_menu/` — 安裝 setuptools / build / wheel --- diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index e87a4497..50ab60f8 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -920,3 +920,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2401 tests in 131 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py`, `test/test_utils/test_install_menu.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-302 · 2026-09-24 · Install TestPioneer from the Install menu · #feature #menu + +- **What**: the Automation menu runs TestPioneer, but Install ▸ Automation had an entry for every other automation module and none for it, so it could not be installed or upgraded from the IDE. An environment with test_pioneer older than 0.1.34 reads a YAML file in the locale's encoding and fails on a non-ASCII character on a cp950 Windows (progress #106); the entry is a way out of that without a terminal. +- **Change**: `Install TestPioneer` (`安裝 TestPioneer`) after MailThunder in `PYPI_PACKAGES`, running `pip install -U test_pioneer` in a run window like the others. +- **Tests**: `TestTheAutomationInstallMenu` expects the entry and its package, after MailThunder, prthinker still last. +- **Result / numbers**: 2401 tests in 131 files. The full suite at c8741d6 (before this and U-20260924-301): 2385 passed, 15 skipped; both startup tests exit 0; gates clean. `ruff check` clean. +- **Docs**: `images/menu_install.png` redone from the real submenu under dark_amber (it was 259 × 312, now 220 × 356 with the extra entry: the new one is rendered, not edited); the three READMEs' TestPioneer row; `architecture_explore.md` §5.5; progress #106 names the entry. +- **Files**: `pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_install_menu.py`, `images/menu_install.png`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md`, `progress.md` +- **Open items**: #106 (unchanged: whether the dependency should require 0.1.34). diff --git a/docs/updates/README.md b/docs/updates/README.md index a40fc543..b76d0c4f 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-302 | 2026-09-24 | Install TestPioneer from the Install menu | #feature #menu | [2026-09-b](2026-09-b.md) | | U-20260924-301 | 2026-09-24 | Build the automation Install menu from a table | #refactor #menu | [2026-09-b](2026-09-b.md) | | U-20260924-300 | 2026-09-24 | Correction to U-20260924-295: the keyword colours are not shown yet | #docs #jeditor | [2026-09-b](2026-09-b.md) | | U-20260924-299 | 2026-09-24 | clear and reset wipe the SSH terminal | #fix #ssh | [2026-09-b](2026-09-b.md) | @@ -383,4 +384,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 99 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 100 | diff --git a/images/menu_install.png b/images/menu_install.png index 599b62a62178c96d6ee3422a42f69c353960f2f4..5cd5650ed0d0572241cd75957a8abd81de09ef13 100644 GIT binary patch literal 7863 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 Q9La+uMdd_FgkHb=0.1.34`? +- **#106** [DECIDE] `test_pioneer` is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), and before 0.1.34 it read a YAML file in the locale's encoding: on a Traditional Chinese Windows (cp950) TestPioneer "Run YAML" of a file with a non-ASCII character, which the editor saves as UTF-8, exits 1 with `UnicodeDecodeError` (`extend/process_executor/test_pioneer/test_pioneer_process_manager.py`). This repo's `.venv` has 0.1.30; 0.1.34 reads it as UTF-8. `Install ▸ Automation ▸ Install TestPioneer` upgrades it by hand meanwhile. Should the dependencies require `test_pioneer>=0.1.34`? - **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 0e840aaa..38cf9221 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -104,6 +104,7 @@ "install_menu_webrunner": "Install WebRunner", "install_menu_automation_file": "Install Automation File", "install_menu_mail_thunder": "Install MailThunder", + "install_menu_test_pioneer": "Install TestPioneer", "install_menu_prthinker": "Install prthinker (code review)", "install_menu_tools_install_menu_label": "Tools", "install_menu_tools_install_build_tools": "Install Build Tools", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 5204e0da..4ea33b16 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -99,6 +99,7 @@ "install_menu_webrunner": "安裝 WebRunner", "install_menu_automation_file": "安裝 Automation File", "install_menu_mail_thunder": "安裝 MailThunder", + "install_menu_test_pioneer": "安裝 TestPioneer", "install_menu_prthinker": "安裝 prthinker(程式碼審查)", "install_menu_tools_install_menu_label": "工具", "install_menu_tools_install_build_tools": "安裝 Build Tools", diff --git a/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py b/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py index 412469e7..43cba055 100644 --- a/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py +++ b/pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py @@ -24,6 +24,7 @@ ("install_menu_webrunner", "je_web_runner"), ("install_menu_automation_file", "automation_file"), ("install_menu_mail_thunder", "je_mail_thunder"), + ("install_menu_test_pioneer", "test_pioneer"), ) diff --git a/test/test_utils/test_install_menu.py b/test/test_utils/test_install_menu.py index 6b1538d6..b9556e25 100644 --- a/test/test_utils/test_install_menu.py +++ b/test/test_utils/test_install_menu.py @@ -144,6 +144,7 @@ def test_the_entries_and_their_packages(self, app, processes): (words.get("install_menu_webrunner"), "je_web_runner"), (words.get("install_menu_automation_file"), "automation_file"), (words.get("install_menu_mail_thunder"), "je_mail_thunder"), + (words.get("install_menu_test_pioneer"), "test_pioneer"), ] assert menu.actions()[-1].text() == words.get("install_menu_prthinker") From 0e8ccbe9645a1d50c707c4a4bd223094256bbc31 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:12:03 +0800 Subject: [PATCH 034/166] Make the automation menus' HELP builder public --- docs/updates/2026-09-b.md | 7 +++++++ docs/updates/README.md | 3 ++- .../menu/automation_menu/automation_menu_factory.py | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 50ab60f8..961f42db 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -930,3 +930,10 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `images/menu_install.png` redone from the real submenu under dark_amber (it was 259 × 312, now 220 × 356 with the extra entry: the new one is rendered, not edited); the three READMEs' TestPioneer row; `architecture_explore.md` §5.5; progress #106 names the entry. - **Files**: `pybreeze/pybreeze_ui/menu/install_menu/automation_menu/build_automation_install_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_install_menu.py`, `images/menu_install.png`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md`, `progress.md` - **Open items**: #106 (unchanged: whether the dependency should require 0.1.34). + +## U-20260924-303 · 2026-09-24 · Make the automation menus' HELP builder public · #refactor #menu + +- **What** (refactor, no change in behaviour): `automation_menu_factory._add_help_menu` is now `add_help_menu`, with a docstring, so a menu not built by `build_automation_menu` (TestPioneer's) can add the same HELP submenu instead of copying it. +- **Tests**: `test_automation_menu_factory.py` (4) passes unchanged. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b76d0c4f..aae18519 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-303 | 2026-09-24 | Make the automation menus' HELP builder public | #refactor #menu | [2026-09-b](2026-09-b.md) | | U-20260924-302 | 2026-09-24 | Install TestPioneer from the Install menu | #feature #menu | [2026-09-b](2026-09-b.md) | | U-20260924-301 | 2026-09-24 | Build the automation Install menu from a table | #refactor #menu | [2026-09-b](2026-09-b.md) | | U-20260924-300 | 2026-09-24 | Correction to U-20260924-295: the keyword colours are not shown yet | #docs #jeditor | [2026-09-b](2026-09-b.md) | @@ -384,4 +385,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 100 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 101 | diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py b/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py index 8bbc38d5..14b3b65b 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py @@ -67,7 +67,7 @@ def build_automation_menu(ui: PyBreezeMainWindow, spec: AutomationMenu) -> QMenu if spec.run_actions: _add_run_menu(menu, spec.run_actions) if spec.help_links: - _add_help_menu(ui, menu, spec.help_links) + add_help_menu(ui, menu, spec.help_links) if spec.create_project and spec.create_project_label_key: _add_project_menu(menu, spec.create_project, spec.create_project_label_key) if spec.gui_widget_class and spec.gui_label: @@ -84,7 +84,8 @@ def _add_run_menu(menu: QMenu, run_actions: tuple[RunAction, ...]) -> None: run_menu.addAction(action) -def _add_help_menu(ui: PyBreezeMainWindow, menu: QMenu, links: tuple[HelpLink, ...]) -> None: +def add_help_menu(ui: PyBreezeMainWindow, menu: QMenu, links: tuple[HelpLink, ...]) -> None: + """Add a HELP submenu to *menu*: one entry per link, each opening its page in a browser tab.""" lang = language_wrapper.language_word_dict help_menu = menu.addMenu(lang.get("help_label")) for link in links: From 94f74fe14408d99a83309648c814e469ef58c9a0 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:17:38 +0800 Subject: [PATCH 035/166] Give the TestPioneer menu a HELP submenu --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 ++-- docs/updates/2026-09-b.md | 10 +++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_english.py | 2 ++ .../extend_traditional_chinese.py | 2 ++ .../build_test_pioneer_menu.py | 7 +++++++ test/test_utils/test_testpioneer_menu.py | 21 +++++++++++++++++++ 10 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4150699e..c23c6c75 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Password or private-key authentication (the key file picked with Browse, startin - **File Tree Context Menu** — right-click to create, rename, delete, copy absolute or relative paths, or reveal the item in your platform file manager (a file shown selected in Explorer and Finder). Renaming or deleting a file open in an editor tab keeps the tab in sync. - **Package Manager** — install automation modules and build tools from the menu, output in a run window. -- **Integrated Documentation** — each module's docs and GitHub page open as in-IDE browser tabs. +- **Integrated Documentation** — each module's docs and GitHub page open as in-IDE browser tabs (TestPioneer's documentation is its GitHub README). --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index c924383e..3675a631 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -155,7 +155,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: - **文件树右键菜单** — 右键即可创建、重命名、删除、复制绝对或相对路径,或在系统的文件管理器中显示该项目(在 Explorer 与 Finder 中会选中该文件)。重命名或删除一个已在编辑器标签页中打开的文件时,标签页会保持同步。 - **包管理器** — 从菜单安装自动化模块与构建工具,输出显示在运行窗口中。 -- **集成文档** — 每个模块的文档与 GitHub 页面都以 IDE 内的浏览器标签页打开。 +- **集成文档** — 每个模块的文档与 GitHub 页面都以 IDE 内的浏览器标签页打开(TestPioneer 的文档就是它 GitHub 上的 README)。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index a85edeb6..e5ac639f 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -155,7 +155,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: - **檔案樹右鍵選單** — 按右鍵即可建立、重新命名、刪除、複製絕對或相對路徑,或在系統的檔案管理員中顯示該項目(在 Explorer 與 Finder 中會選取該檔案)。重新命名或刪除已在編輯器分頁中開啟的檔案時,分頁會同步更新。 - **套件管理員** — 從選單安裝自動化模組與建置工具,輸出顯示在執行視窗中。 -- **整合文件** — 每個模組的文件與 GitHub 頁面都以 IDE 內的瀏覽器分頁開啟。 +- **整合文件** — 每個模組的文件與 GitHub 頁面都以 IDE 內的瀏覽器分頁開啟(TestPioneer 的文件就是它 GitHub 上的 README)。 --- diff --git a/architecture_explore.md b/architecture_explore.md index 1cacbc5d..9b11309f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -185,7 +185,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 5.2 非工廠的兩個選單 -- **`test_pioneer_menu/`** — 建範本目錄(寫在 IDE 的工作目錄,已有範本先問是否取代,寫入失敗跳警告)+ `QFileDialog` 選 `.yml` / `.yaml`(副檔名清單與語法高亮共用 `syntax_keyword.TEST_PIONEER_SUFFIXES`;會驗副檔名,選錯跳 `QMessageBox`) +- **`test_pioneer_menu/`** — 建範本目錄(寫在 IDE 的工作目錄,已有範本先問是否取代,寫入失敗跳警告)+ `QFileDialog` 選 `.yml` / `.yaml`(副檔名清單與語法高亮共用 `syntax_keyword.TEST_PIONEER_SUFFIXES`;會驗副檔名,選錯跳 `QMessageBox`)+ HELP 子選單(`add_help_menu`,只有 GitHub:它的 readthedocs 網站沒有建出來) - **`prthinker_menu/`** — 審查目前檔案(先照 Run with... 的方式存檔:`save_current_file_for_run()`)/ 審查 PR(`QInputDialog` 問編號,範圍 1–1,000,000)/ 設定對話框 / Help ### 5.3 `tools/tools_menu.py` — 表格驅動的工具註冊 @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2401 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2402 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 961f42db..c2e78f73 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -937,3 +937,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Tests**: `test_automation_menu_factory.py` (4) passes unchanged. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py` - **Open items**: none. + +## U-20260924-304 · 2026-09-24 · A HELP submenu for TestPioneer · #feature #menu + +- **What**: every automation menu has a HELP submenu opening the module's documentation and GitHub page in a browser tab, except TestPioneer's, so the README's "each module's docs and GitHub page" was not so for it. +- **Change**: `set_test_pioneer_menu` adds a HELP submenu through `automation_menu_factory.add_help_menu` (U-20260924-303) with one entry, `Open TestPioneer GitHub` (`開啟 TestPioneer GitHub`). No documentation entry: TestPioneer's README links `testpioneer.readthedocs.io`, and every address under it answers 404 today (`/`, `/en/latest/`, `/en/stable/`, the API's project record), so its GitHub README is its manual. That broken link is TestPioneer's to fix (its README and `.readthedocs.yaml`); recorded here, not fixed. The other help links in `pybreeze/` and all the links in the three READMEs answer 200. +- **Tests**: `test_its_help_opens_the_github_page` in `test_testpioneer_menu.py` builds the menu and triggers the HELP entry; it failed before the change (no submenu). +- **Result / numbers**: 2402 tests in 131 files. `ruff check` clean. +- **Docs**: the three READMEs' Integrated Documentation line; `architecture_explore.md` (`test_pioneer_menu/`, test count). +- **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_testpioneer_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: TestPioneer's documentation site (its repository). diff --git a/docs/updates/README.md b/docs/updates/README.md index aae18519..2f5401cc 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-304 | 2026-09-24 | A HELP submenu for TestPioneer | #feature #menu | [2026-09-b](2026-09-b.md) | | U-20260924-303 | 2026-09-24 | Make the automation menus' HELP builder public | #refactor #menu | [2026-09-b](2026-09-b.md) | | U-20260924-302 | 2026-09-24 | Install TestPioneer from the Install menu | #feature #menu | [2026-09-b](2026-09-b.md) | | U-20260924-301 | 2026-09-24 | Build the automation Install menu from a table | #refactor #menu | [2026-09-b](2026-09-b.md) | @@ -385,4 +386,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 101 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 102 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 38cf9221..fb2c0be3 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -116,6 +116,8 @@ "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", # Test Pioneer Menu "test_pioneer_label": "TestPioneer", + "test_pioneer_github_label": "Open TestPioneer GitHub", + "test_pioneer_github_tab_label": "TestPioneer GitHub", "test_pioneer_create_template_label": "Create TestPioneer Yaml template", "test_pioneer_template_exists": "{path} already exists. Replace it with a fresh template and lose its content?", "test_pioneer_template_failed": "The template could not be created at {path}: {error}", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 4ea33b16..c348c8c9 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -111,6 +111,8 @@ "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", # Test Pioneer Menu "test_pioneer_label": "TestPioneer", + "test_pioneer_github_label": "開啟 TestPioneer GitHub", + "test_pioneer_github_tab_label": "TestPioneer GitHub", "test_pioneer_create_template_label": "建立 TestPioneer Yaml 模板", "test_pioneer_template_exists": "{path} 已經存在。要用新的範本取代,並捨棄它目前的內容嗎?", "test_pioneer_template_failed": "無法在 {path} 建立範本:{error}", diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py index 3d6956b5..eb458970 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py @@ -11,6 +11,7 @@ from pybreeze.extend.process_executor.test_pioneer.test_pioneer_process_manager import \ init_and_start_test_pioneer_process +from pybreeze.pybreeze_ui.menu.automation_menu.automation_menu_factory import HelpLink, add_help_menu from pybreeze.pybreeze_ui.syntax.syntax_keyword import TEST_PIONEER_SUFFIXES from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.pybreeze_ui.plain_text import as_text @@ -22,6 +23,11 @@ _YAML_FILTER = "YAML ({})".format(" ".join(f"*{suffix}" for suffix in TEST_PIONEER_SUFFIXES)) # Where TestPioneer puts its template, under the project directory _TEMPLATE_DIR = ".TestPioneer" +# Its README is its manual: the readthedocs site it links to is not built +_HELP_LINKS = ( + HelpLink("https://github.com/Integration-Automation/TestPioneer", + "test_pioneer_github_label", "test_pioneer_github_tab_label"), +) def set_test_pioneer_menu(ui_we_want_to_set: PyBreezeMainWindow): @@ -50,6 +56,7 @@ def set_test_pioneer_menu(ui_we_want_to_set: PyBreezeMainWindow): ui_we_want_to_set.test_pioneer_menu.addAction( ui_we_want_to_set.run_yaml_action ) + add_help_menu(ui_we_want_to_set, ui_we_want_to_set.test_pioneer_menu, _HELP_LINKS) def create_template(ui_we_want_to_set: PyBreezeMainWindow) -> None: diff --git a/test/test_utils/test_testpioneer_menu.py b/test/test_utils/test_testpioneer_menu.py index 9d7a42a0..fab4d028 100644 --- a/test/test_utils/test_testpioneer_menu.py +++ b/test/test_utils/test_testpioneer_menu.py @@ -84,3 +84,24 @@ def test_cancelling_does_nothing(app, chosen): menu.check_file(None) assert chosen["ran"] == [] and chosen["told"] == 0 + + +def test_its_help_opens_the_github_page(app, monkeypatch): + # Every other automation menu has a HELP submenu; TestPioneer's had none. + from types import SimpleNamespace + + from PySide6.QtWidgets import QMenu + + from pybreeze.pybreeze_ui.menu.automation_menu import automation_menu_factory + + opened: list = [] + monkeypatch.setattr( + automation_menu_factory, "open_web_browser", lambda _ui, url, label: opened.append((url, label))) + window = SimpleNamespace(automation_menu=QMenu()) + menu.set_test_pioneer_menu(window) + + (help_menu,) = [action.menu() for action in window.test_pioneer_menu.actions() if action.menu()] + for action in help_menu.actions(): + action.trigger() + + assert opened == [("https://github.com/Integration-Automation/TestPioneer", "TestPioneer GitHub")] From 630f78bc35edca6fc787453dcc8152bab9a6b5be Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:19:10 +0800 Subject: [PATCH 036/166] Use Taiwan terms for Help and template in Traditional Chinese --- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_traditional_chinese.py | 8 ++++---- test/test_utils/test_language_parity.py | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index c2e78f73..9fb5bb18 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -947,3 +947,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: the three READMEs' Integrated Documentation line; `architecture_explore.md` (`test_pioneer_menu/`, test count). - **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_testpioneer_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: TestPioneer's documentation site (its repository). + +## U-20260924-305 · 2026-09-24 · Taiwan terms for Help and template in the Traditional Chinese IDE · #fix #i18n + +- **What**: the Traditional Chinese IDE called every automation menu's HELP submenu 幫助, and three entries said 模板 where the rest of the dictionary says 範本 (the TestPioneer template entry sat above messages about the same file calling it 範本). +- **Fix**: `help_label` is 說明; `test_pioneer_create_template_label`, `prompt_editor_switch_over_edits` and `prompt_editor_create_over_edits` say 範本. +- **Tests**: `test_traditional_chinese_uses_taiwan_terms` also looks for 幫助 → 說明 and 模板 → 範本; it failed on those four entries before the change. +- **Result / numbers**: 2402 tests in 131 files (one test widened, none added). `test_language_parity.py` passes. +- **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py` +- **Open items**: #108 (JEditor's own entries, unchanged). diff --git a/docs/updates/README.md b/docs/updates/README.md index 2f5401cc..acfdb00d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-305 | 2026-09-24 | Taiwan terms for Help and template in the Traditional Chinese IDE | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-304 | 2026-09-24 | A HELP submenu for TestPioneer | #feature #menu | [2026-09-b](2026-09-b.md) | | U-20260924-303 | 2026-09-24 | Make the automation menus' HELP builder public | #refactor #menu | [2026-09-b](2026-09-b.md) | | U-20260924-302 | 2026-09-24 | Install TestPioneer from the Install menu | #feature #menu | [2026-09-b](2026-09-b.md) | @@ -386,4 +387,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 102 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 103 | diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index c348c8c9..7176f464 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -16,7 +16,7 @@ "install_menu_label": "安裝", # Normal label "run_label": "執行", - "help_label": "幫助", + "help_label": "說明", "project_label": "專案", "create_project_exists": "{path} 已經存在。要用新的範本檔取代,並捨棄你對它們的修改嗎?", "create_project_failed": "無法在 {path} 建立專案:{error}", @@ -113,7 +113,7 @@ "test_pioneer_label": "TestPioneer", "test_pioneer_github_label": "開啟 TestPioneer GitHub", "test_pioneer_github_tab_label": "TestPioneer GitHub", - "test_pioneer_create_template_label": "建立 TestPioneer Yaml 模板", + "test_pioneer_create_template_label": "建立 TestPioneer Yaml 範本", "test_pioneer_template_exists": "{path} 已經存在。要用新的範本取代,並捨棄它目前的內容嗎?", "test_pioneer_template_failed": "無法在 {path} 建立範本:{error}", "test_pioneer_template_created": "已建立範本:{path}", @@ -222,11 +222,11 @@ "prompt_editor_not_utf8": "{filename} 不是 UTF-8 文字檔。讀不出來的字元已經換掉後顯示;存檔時會改存成 UTF-8。", "prompt_editor_switch_over_edits": - "{filename} 還有沒存的編輯。要切換模板並捨棄它們嗎?", + "{filename} 還有沒存的編輯。要切換範本並捨棄它們嗎?", "prompt_editor_reload_button_over_edits": "{filename} 還有沒存的編輯。要從磁碟重新載入並捨棄它們嗎?", "prompt_editor_create_over_edits": - "要用內建模板建立 {filename} 嗎?這裡輸入的內容會被取代。", + "要用內建範本建立 {filename} 嗎?這裡輸入的內容會被取代。", "prompt_editor_unreadable": "{filename} 讀不出來:{error}", "prompt_editor_close_over_edits": diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 009fa311..e4ef08db 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -72,7 +72,8 @@ def test_traditional_chinese_uses_taiwan_terms(self): # The automation menus said 運行 where the run window said 執行; JEditor's # own entries (its Run and plugin menus) are not in this dictionary mainland = {"運行": "執行", "字體": "字型", "插件": "外掛", "默認": "預設", "文件夾": "資料夾", - "信息": "訊息", "軟件": "軟體", "數據": "資料", "屏幕": "螢幕", "鼠標": "滑鼠"} + "信息": "訊息", "軟件": "軟體", "數據": "資料", "屏幕": "螢幕", "鼠標": "滑鼠", + "幫助": "說明", "模板": "範本"} found = { key: [word for word in mainland if word in value] for key, value in ZH.items() From 401adc2b060f55ac9f25e8412e3399915855eea1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:22:17 +0800 Subject: [PATCH 037/166] Record the gitpython floor question --- docs/updates/2026-09-b.md | 8 ++++++++ docs/updates/README.md | 3 ++- progress.md | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 9fb5bb18..c80fb57c 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -956,3 +956,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2402 tests in 131 files (one test widened, none added). `test_language_parity.py` passes. - **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py` - **Open items**: #108 (JEditor's own entries, unchanged). + +## U-20260924-306 · 2026-09-24 · Record the gitpython floor question as progress #109 · #docs #deps #security + +- **What**: je-editor 1.0.28 (on PyPI today) changes nothing but its metadata: its wheel's files are identical to 1.0.27's, and it requires `gitpython>=3.1.59`, which fixes CVE-2026-78676 (a config write turning a quoted value into a live `core.hooksPath`, remote code execution) and CVE-2026-78679 (`TagReference.create` reading local files). 3.1.59 still has CVE-2026-87818 (`--no-index` through the diff API reads any path), fixed in 3.1.60. PyBreeze requires `je-editor>=1.0.27`, so an existing environment keeps whatever gitpython it has; the IDE's git features (JEditor's `git_client`, file baseline and blame) use it. This repo's `.venv` has 3.1.62. +- **Recorded**: progress #109 [DECIDE]: raise the floor to `je-editor>=1.0.28`, declare `gitpython>=3.1.60` here, or both. JEditor's own floor of 3.1.59 (`D:\Codes\JEDITOR\pyproject.toml:18`) is JEditor's to raise; not changed from here. +- **Also checked**: je-mail-thunder 0.0.29 (also today) still gives `SMTPWrapper` no timeout, so #102 stays blocked; je-editor 1.0.28 does not change the generic highlighter or the Traditional Chinese words, so #103 and #108 stay blocked. +- **Files**: `progress.md` +- **Open items**: #109. diff --git a/docs/updates/README.md b/docs/updates/README.md index acfdb00d..dbb2000a 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-306 | 2026-09-24 | Record the gitpython floor question as progress #109 | #docs #deps #security | [2026-09-b](2026-09-b.md) | | U-20260924-305 | 2026-09-24 | Taiwan terms for Help and template in the Traditional Chinese IDE | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-304 | 2026-09-24 | A HELP submenu for TestPioneer | #feature #menu | [2026-09-b](2026-09-b.md) | | U-20260924-303 | 2026-09-24 | Make the automation menus' HELP builder public | #refactor #menu | [2026-09-b](2026-09-b.md) | @@ -387,4 +388,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 103 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 104 | diff --git a/progress.md b/progress.md index ef99768c..9001491c 100644 --- a/progress.md +++ b/progress.md @@ -17,3 +17,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#103** [BLOCKED] PyBreeze's automation keywords are never highlighted: `syntax_extend_package` registers them for `.json`, `.yml` and `.yaml` (`pybreeze_ui/syntax/syntax_extend.py`), but JEditor has built-in rules for those suffixes, so `CodeEditor.reset_highlighter` gets a `GenericHighlighter` from `highlighter_for`, and only `PythonHighlighter` reads the plugin registry (`je_editor/pyside_ui/code/syntax/generic_syntax.py:124`, `code_edit_plaintext.py:423-425` in `D:\Codes\JEDITOR` at d372c35). Needs JEditor's generic highlighter to add a registered language's words for the suffix, taking the colour as a theme colour key as its own rules do (PyBreeze registers `warning_output_color` and `diff_modified_marker_color`); the `architecture.md` §6 contract changes with it. - **#106** [DECIDE] `test_pioneer` is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), and before 0.1.34 it read a YAML file in the locale's encoding: on a Traditional Chinese Windows (cp950) TestPioneer "Run YAML" of a file with a non-ASCII character, which the editor saves as UTF-8, exits 1 with `UnicodeDecodeError` (`extend/process_executor/test_pioneer/test_pioneer_process_manager.py`). This repo's `.venv` has 0.1.30; 0.1.34 reads it as UTF-8. `Install ▸ Automation ▸ Install TestPioneer` upgrades it by hand meanwhile. Should the dependencies require `test_pioneer>=0.1.34`? - **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. +- **#109** [DECIDE] `je-editor>=1.0.27` (`requirements.txt`, `pyproject.toml`, `dev.toml`) lets an environment keep a gitpython below 3.1.59, which has CVE-2026-78676 (a config write turns a quoted value into a live `core.hooksPath`: code runs on the next hook) and CVE-2026-78679; the IDE's git features use it (JEditor's `je_editor/git_client/`). je-editor 1.0.28 differs from 1.0.27 only in requiring `gitpython>=3.1.59`, and 3.1.59 still has CVE-2026-87818 (the diff API's `--no-index` reads any path), fixed in 3.1.60. Should the dependencies say `je-editor>=1.0.28`, declare `gitpython>=3.1.60`, or both? JEditor's own floor is its to raise (`D:\Codes\JEDITOR\pyproject.toml:18`). From 54856a9184564883da67258be33fd4073c8eaba4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:26:40 +0800 Subject: [PATCH 038/166] Redo the README screenshots of the AI tabs --- docs/updates/2026-09-b.md | 7 +++++++ docs/updates/README.md | 3 ++- images/ai_code_review.png | Bin 28770 -> 31930 bytes images/cot_prompt_editor.png | Bin 38931 -> 46259 bytes images/skill_prompt_editor.png | Bin 45657 -> 53768 bytes images/skills_send.png | Bin 31409 -> 34381 bytes 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index c80fb57c..05f5bea1 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -964,3 +964,10 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Also checked**: je-mail-thunder 0.0.29 (also today) still gives `SMTPWrapper` no timeout, so #102 stays blocked; je-editor 1.0.28 does not change the generic highlighter or the Traditional Chinese words, so #103 and #108 stay blocked. - **Files**: `progress.md` - **Open items**: #109. + +## U-20260924-307 · 2026-09-24 · Redo the README screenshots of the AI tabs · #docs #readme + +- **What**: rendered each AI tab bare at its README image size under dark_amber and compared. `ai_code_review.png` showed Accept and Reject enabled before any response; they are now disabled until one arrives. `cot_prompt_editor.png` and `skill_prompt_editor.png` lacked the line naming the prompt folder ("Prompt files (these override the built-in prompts): ..."). `skills_send.png` matched in content but sat 2 px off the current layout. +- **Done**: all four redone at their old sizes (1080 × 640, 1000 × 620, 1000 × 620, 1000 × 700) with the same content: the same URLs and the same code in the review client, the built-in `first_code_review.md` and `code_review_skill.md` in the editors. The editors ran with `USERPROFILE` set to a throwaway `D:\Users\ada` holding the built-in templates as Create File writes them, so the folder line names no real account; the folder was removed afterwards. The other tool images (cURL, HAR, hash, timestamp, regex, HTTP status, JWT, JSON format, header analyzer, response inspector) match the current tabs and are unchanged. +- **Files**: `images/ai_code_review.png`, `images/cot_prompt_editor.png`, `images/skill_prompt_editor.png`, `images/skills_send.png` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index dbb2000a..de1d1506 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-307 | 2026-09-24 | Redo the README screenshots of the AI tabs | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-306 | 2026-09-24 | Record the gitpython floor question as progress #109 | #docs #deps #security | [2026-09-b](2026-09-b.md) | | U-20260924-305 | 2026-09-24 | Taiwan terms for Help and template in the Traditional Chinese IDE | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-304 | 2026-09-24 | A HELP submenu for TestPioneer | #feature #menu | [2026-09-b](2026-09-b.md) | @@ -388,4 +389,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 104 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 105 | diff --git a/images/ai_code_review.png b/images/ai_code_review.png index ffee085c0316454c14c225875f43f1cc22793e4e..984a7444d25c9987ac527c5301288c4c56e3a910 100644 GIT binary patch literal 31930 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%0D((nkCJWWU|B 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 F{a>krrjP&t literal 28770 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

63{iBR{o)8npOu7;ZcWW?k& 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�&6 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 S(p-`@c*^qXa%D0v-~SIw6LB~I diff --git a/images/skill_prompt_editor.png b/images/skill_prompt_editor.png index ee640c650e836cd357bb6eeaeec75294c480fd71..8c98a79b3d902d346d4688407a3bd61f0316da02 100644 GIT binary patch literal 53768 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

=r4tEeJv}`(Kh`O&`iw1eYa|5(;=hIt4{7=O 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 Idrw~e3u+iK1ONa4 diff --git a/images/skills_send.png b/images/skills_send.png index 75136f2aae7ff375a311e37260bf337150609e8d..5ca71509d54a7bdba647592dd55e8b491eafed22 100644 GIT binary patch literal 34381 zcmcG$2UJsA+b+u9wrxSIAfTYqq;Gl$l_p(UfY21A_g({v1*M48&^v@4DWMZo1f)YK zp(7wIA%v0;NPwIL``i2c|M<@T-*fJ{*D)9@&B|JHuKB*te4gihmmi*LtI}Rzx97ni5CscPWpBIHd}u4-+{Z+&@6SE z&Hi?BM03si>EK+y!+y22OTafH0 zz6DfGI4gIXw+6l)gInZ?l_ohZ*K}cgbaYZQIn_`#UDrqV8=WE&py>(u#8RUN^70j5 zcHp%Bx;hG5_3RG7_XBL;W>pUt6^^nWQlcg=9*^dwf_4`pfyc1laF{dtqAbIW8~Ion zOD@>(nvX*DL|K1EMeIyw0k5cd+YAfsOGC8d^PmJnJ>%x>};959@M71kT{!VP)2u_9I0@=kJ?) zKbJEs1-CEy+E$w|jL2HzCTqr$%1t%Ic0YZ(;)6q}#a_KSS>=*8U+duD5a{#s?&0A; zYXQ^AM1J-%c=PZ``K~8}$_^|f%;rq+kiFOvqte1jgy3KvHAo$@(bTypUs);SIMtZA zQBRS-BNwPy8fsRGZSqEK#^&wRGH#7MQ`-<(rR?I-&29|#2X6#5IF_fYI2`7Xhk<*Q zR0p&G-j3qMZ;Grkz?{TUx;d~s-cFwl;|sO*(O8)-tIDFOXtY!Rv|D0QQsFZ}yFnSt zplIKX$#R$0Ljko^(oC>VV_{xcn07GiQ&5I~yvF3}q=1CONOGm8C_H z;|~2|^~&)xq)Cs`kIPQ$)z^${h8+Z%;P zm>$6g;^Hd6vy(42{>UQ32^%i1A0PjHptG}mZF8PYxz@4)bMBCWzdRFQN{TOGIJpBG zjNmuOrE>@#kR>V{?oxbRsEa!ADKW?KhbsmZ`Mhvku1cTj(jloY$^;YGLP~4j|K?if z)ZUFJuC~#Y=(R^&xJYZbg00YPJ4-~kZ(WO_G?V64F++#8?HwIAOhd3qP$ml_y5pO@ z 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 literal 31409 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 From abfdb95d0886ff6adb9d5f26b292d29b911acb41 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:29:39 +0800 Subject: [PATCH 039/166] Name the chosen target on the cURL import button --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/tool_curl_import.png | Bin 47967 -> 52029 bytes images/tool_curl_import_action.png | Bin 49626 -> 54704 bytes .../extend_multi_language/extend_english.py | 2 +- .../extend_traditional_chinese.py | 2 +- .../pybreeze_ui/tools_gui/curl_import_gui.py | 12 ++++++++++-- test/test_utils/test_curl_import_gui.py | 6 ++++++ 9 files changed, 31 insertions(+), 6 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 9b11309f..ce2c467f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -457,7 +457,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2402 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2403 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 05f5bea1..4de748bd 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -971,3 +971,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Done**: all four redone at their old sizes (1080 × 640, 1000 × 620, 1000 × 620, 1000 × 700) with the same content: the same URLs and the same code in the review client, the built-in `first_code_review.md` and `code_review_skill.md` in the editors. The editors ran with `USERPROFILE` set to a throwaway `D:\Users\ada` holding the built-in templates as Create File writes them, so the folder line names no real account; the folder was removed afterwards. The other tool images (cURL, HAR, hash, timestamp, regex, HTTP status, JWT, JSON format, header analyzer, response inspector) match the current tabs and are unchanged. - **Files**: `images/ai_code_review.png`, `images/cot_prompt_editor.png`, `images/skill_prompt_editor.png`, `images/skills_send.png` - **Open items**: none. + +## U-20260924-308 · 2026-09-24 · The cURL import button names the chosen target · #fix #tools #readme + +- **What**: the cURL import tab's button said "Convert to Python requests" ("轉換為 Python requests") whatever was chosen under Generate for, so with pytest, APITestka or LoadDensity chosen it named the wrong output; both README screenshots showed it that way. +- **Fix**: `curl_import_convert_button` is `Generate {target}` (`產生 {target}`), filled with the chosen target's label when the tab opens and whenever the choice changes (`CurlImportGUI._name_convert_button`). +- **Tests**: `test_the_button_names_the_chosen_target` in `test_curl_import_gui.py` picks each target and expects its label on the button; it failed on the second target before the change. +- **Result / numbers**: 2403 tests in 131 files. `test_curl_import_gui.py`, `test_language_parity.py` and `test_tools_menu_docks.py` pass (60). `ruff check` clean. +- **Docs**: `images/tool_curl_import.png` and `images/tool_curl_import_action.png` redone at 900 × 720 with the same command and targets (pytest, APITestka JSON action); `architecture_explore.md` test count. The README text does not name the button. +- **Files**: `pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_curl_import_gui.py`, `images/tool_curl_import.png`, `images/tool_curl_import_action.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index de1d1506..2d2133b1 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-308 | 2026-09-24 | The cURL import button names the chosen target | #fix #tools #readme | [2026-09-b](2026-09-b.md) | | U-20260924-307 | 2026-09-24 | Redo the README screenshots of the AI tabs | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-306 | 2026-09-24 | Record the gitpython floor question as progress #109 | #docs #deps #security | [2026-09-b](2026-09-b.md) | | U-20260924-305 | 2026-09-24 | Taiwan terms for Help and template in the Traditional Chinese IDE | #fix #i18n | [2026-09-b](2026-09-b.md) | @@ -389,4 +390,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 105 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 106 | diff --git a/images/tool_curl_import.png b/images/tool_curl_import.png index 0da7990df373ddbaaa946b439ec58d1a2263d0d2..7aa8d540a374e30986167fdb79af662df25953f9 100644 GIT binary patch literal 52029 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

ZpqsL>g~jyEr2foAl7wKMhJ> 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

adtb&wgDsV+dIbjD6K_+#3N_c2ic{s8|&aAGXEq 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 bo^b0mkl8+`>;aw#2=h@yMz~N==iC1Qmdcbq literal 47967 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%0Dmv;-}sM}vfwcsz!|vryX=x4jqvSI>#P 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

  • (1+go*WBjSO$#ZGZ@&+;KGK2;3hi=U4i-wKL@ zQ#2nbED3oYa^wUbL6#pWNIa@+h@EPr#F`H;tvGn?(vmtP#jM^LNXJB#1(1X{gQ8*gIEt16Z{V#4!w^iNF2kp#xF-!ZRvRi) zudOZqts79kX|rZjdPKJ_6eIWV{ec&?9{WY}G%JaUIYjnCk%`=k8!X)h8<-1ja&`PiVM2GS{UBw)efH_eRw?<5j>wlN9QL%|=&-|Z>?ida3SJx zIu81ae&rY4gJ%Do=TCcy+y~!mX-h-_-*o6hx-{vPG7j?UC^3Y7{E{{+2{qp#_-X-6 zZg4s!{e!oNL6d+FP9Gs4M^=x2FzhMzBjNXX?C&?<_QRPJylN*)7QdetR{_{&Ysfj> zRtb9q8L$-rI>yTx1+f%i2a|rkFX!)w+{35)JrWwgI%VIwx;Z3Ghd(~2-_nQpM_{CV zH1w2CDvF7rvDB;bb#h$sl=uHqWOcMXSlfxTQUwD}DunXv{JWZeNxbi+e(JOI_@@d2 zj=4uUZd!JfB>WOn{4@J4JaS?YqdA5{`}{xhi&`Bu&5~&5t?)rf{|)3{&$B14bqFr6 zZx`yR$$<;13F*b>2J)8CYW9&$JIYTtMh!OzV#`lEr!~AEFv)V<);paL8e)sP=Fzmf zXlOW6T`$Q3yj#luf!zHfh;v|E}1816$Kvl1R?SF>aAI+B|4%IKKYpz-5 z)42F;q<9c_Z4;HE<1wGl#(G=P$e9ML)NBI~zgDOuAY2^B{`SCwb`67lZ15S^j!xDu zRF)2Fjh}u+8R==5b1ja8W!uh#{(&T97H9h%>F+|(9Rq|xriV-QE+|IV6a#+_XExQ-4j^mhC_!wIO$Xf!A59C9q9l^5Ng;xCPKl2JPUGp*ar$kEU?3#%h80grUlyOb(`p9KBAvGVtltMw_sno()zhkGT zfi5O~lgIw2sCYY5d*&TFy|1<)LZBq1T`Q0)UE=)}?MZ-w+y1`;=@m|dgIX?!o!d?J OlBAd%xcJR`pZ@^eEiCf@ diff --git a/images/tool_curl_import_action.png b/images/tool_curl_import_action.png index 203bbb86abf6bd1a85ecd414283a494a7dadd847..3fdeefc8c92f5f899db7f73af50cbae27512baa8 100644 GIT binary patch literal 54704 zcmeFZRa}&RyDtnPptOp#AfkYDH;jU`fOK~w-8q2L9a2)#-AH#!jda)0&CoHxdn5jz z^{lm?qy5?M**?(GnSpzLbzR@OC-9TBC?*;a8Ug|WrnuM#IRpeG9|VL4zaOCh&-73> zSppA_t;JOB5D>6B?*1Ug&|(oIAiP8n|L|VHIcaCk$r*R-vIVx^Z&^h(WsmbIL!|{p zLVhLx5e*0KL)_K$fFJmXF{!yaLU~Z(tjCXDp@}^c#@WORdi6-+3A)z9$aiRue~p`8 zU6*B=m)F1j^6n6vLKVI@*w4M6#NE!ipTf#2F@jfw^oZOCM-ccc*I`uth1Z{d;s^%F z3q2M5=Y_1-G9Kc+`EQ3{`Xa~JlF&t%+GD)!KeYoN9W z3=KcX?TiK%XHK*ahTu~gbNPm_OHPejMggsJ^28>!_I{`NwKjC32L~t9&oypKIN&+^ z6b-z)76@AtCEecwt;#kF*!vT&Nu|Q#w3R9^CF(cY(4DO=ahon&>4VoHOLZI*4}Sju zdsJD>dr9ZeYB`;(rw*43vkuG2PgS`fA(458FiyA+3gdHxOyWiMqH?Y8pnOcmosH{6 zU$gDDc!9uGV5tfG@Vk-}{&V+Dz3~!t@rC)u8kpbq1$?B_{M?qELfeBdR75_@A>*gy zkigu?CPOT^A%anfF~@3OtFugNxaJ8Nm?`>XTZzT-*t*~78G=zo!dM?@H65KXU=ro; zlC#SHd@87@SbOlr?{1ts7Z=7KMJPscJ{q*9cWNv&A(1hvc5N)Re^Pv2G1VXcq~u_F z+se9Ks=Dmj|3wJV4GY3;9SpAt?#dsX(mT_y zHkhxDk5E51Sdh_m(0zjxW5#-aHaZeABpxrR-+F{vByt|ibuqdup9~dhT_Kb8C-`@7 zuP$Jpt&SM>+0<&#UGLcG>C6pxrNcx!02_3hrjZ5Z+N;#!F&8SCu?F%Qwhfk0_O>5vEkIn(VaDZd3sq zyTpW7ot;4~nzWjk^@#tTv_XxrI{#9-REX%Z2>#J@k1|*N`>#;b`22LJWBmohi)wRf z8SHwt^~)QCeYkcmPQrJtTx&Z#EGI5jT7HsPBGkl~xZ%Q=5^xd|8z2A9$VjP2$SRiZ z8sm{PVUd{E26kVd(oKw1qBS)IQYu8bVP{d>bLjHBl5XGOg}ZY@3KRlPCEJCW{)o}! zRI5Kfh?${EUGNvlAa6BWJ&2?@zrM-6qWxRsRP(>#h(v++Y8$v#2vZ1-F=jkxy`}Nn zl|L&XM8rpUjch`H-MZn0iU>`(@i6b53~4MB3yGCKw0_MREJR2+3s#OZo4{AoqI~=I z7x@BvEtKWvk`}3C0^Lg~BdzrN)r%r{NJx8TR#tkxnN9lG4={gugOTJIemMC@Z4mNk>#S2bgNZ!N3E0)buuf}3sa7oOTDt=covgB9~ zbLHy{^dH$wzkyv%R$1I4dUanOcj=Wy00RkeUVRf49ZjRDZK~e9fQ7hm@y-c2p!DCh zJI`(OnF%lqj0|g<~mJA8a-cAY8@t1V~3+Lso~ni`oC2iR(%wtJiI zoT=`0m1-6mgfSLz<>1*eU}%yhsBStOiM2q<0(R{$A%p44Cd!p1-oN$X`;yOVpL)mJ zj-A@0_=7H$HID<*e37iu(ug`!)14br?2=~Y6lHC2ibFx{$VrMc2I-+5jIHmDWE`yI zbBSSFV#%^&y$d|>cDcNdLASF!Jb`DE{-%0A+&05(Xp44W*Ky?b*KNkY&xz8@qb$Cw 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

    2}26Zjlnr|3rkDUI&)=U?=0B7LqeC@)%R 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 diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index fb2c0be3..cb50f631 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -430,7 +430,7 @@ # cURL Import — Widget "curl_import_input_label": "Paste a curl command:", "curl_import_input_placeholder": "curl 'https://api.example.com/v1/items' -H 'Accept: application/json'", - "curl_import_convert_button": "Convert to Python requests", + "curl_import_convert_button": "Generate {target}", "curl_import_output_label": "Generated code:", "curl_import_error": "Could not parse the curl command: {error}", "curl_import_empty_hint": "Paste a curl command above, then click convert.", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 7176f464..fb5927b0 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -394,7 +394,7 @@ # cURL 匯入 — 介面 "curl_import_input_label": "貼上 curl 指令:", "curl_import_input_placeholder": "curl 'https://api.example.com/v1/items' -H 'Accept: application/json'", - "curl_import_convert_button": "轉換為 Python requests", + "curl_import_convert_button": "產生 {target}", "curl_import_output_label": "產生的程式碼:", "curl_import_error": "無法解析 curl 指令:{error}", "curl_import_empty_hint": "請先在上方貼上 curl 指令,再按轉換。", diff --git a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py index 6ba35ace..da1d8b86 100644 --- a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py @@ -57,7 +57,8 @@ def __init__(self, main_window=None) -> None: self.target_select.addItem(word.get(label_key), target_key) self.target_select.currentIndexChanged.connect(self._on_target_changed) - self.convert_button = QPushButton(word.get("curl_import_convert_button")) + self.convert_button = QPushButton() + self._name_convert_button() self.convert_button.clicked.connect(self.convert) self.output_label = QLabel(word.get("curl_import_output_label")) @@ -100,8 +101,15 @@ def selected_target(self) -> str: """Return the template key of the currently selected target.""" return self.target_select.currentData() + def _name_convert_button(self) -> None: + """Name the chosen target on the button that generates it.""" + self.convert_button.setText( + language_wrapper.language_word_dict.get("curl_import_convert_button").format( + target=self.target_select.currentText())) + def _on_target_changed(self, _index: int) -> None: - """Regenerate when the target changes, if there is already input.""" + """Rename the button, and regenerate if there is already input.""" + self._name_convert_button() if exact_text(self.input_edit).strip(): self.convert() diff --git a/test/test_utils/test_curl_import_gui.py b/test/test_utils/test_curl_import_gui.py index 94638470..8c673765 100644 --- a/test/test_utils/test_curl_import_gui.py +++ b/test/test_utils/test_curl_import_gui.py @@ -32,6 +32,12 @@ def widget(app): class TestCurlImportGUI: + def test_the_button_names_the_chosen_target(self, widget): + # It said "Convert to Python requests" whichever target was chosen + for index in range(widget.target_select.count()): + widget.target_select.setCurrentIndex(index) + assert widget.target_select.itemText(index) in widget.convert_button.text() + def test_convert_produces_code(self, widget): widget.input_edit.setPlainText("curl https://example.com/api") widget.convert() From cca9edd2ed24002dba5781f91584917291eaea7e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 22:54:37 +0800 Subject: [PATCH 040/166] Keep library debug records out of the Code Result panel --- CLAUDE.md | 1 + architecture.md | 1 + architecture_explore.md | 3 +- docs/updates/2026-09-b.md | 10 +++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/code_result_logs.py | 29 +++++++++++++++ pybreeze/pybreeze_ui/editor_main/main_ui.py | 2 + test/test_utils/test_code_result_logs.py | 41 +++++++++++++++++++++ test/test_utils/test_jeditor_contract.py | 2 + 9 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 pybreeze/pybreeze_ui/code_result_logs.py create mode 100644 test/test_utils/test_code_result_logs.py diff --git a/CLAUDE.md b/CLAUDE.md index 9b7b8617..528b2fae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,7 @@ pybreeze/ │ ├── exact_text.py # exact_text: a text box read as typed (toPlainText changes U+00A0, U+2028) │ ├── terminal_view.py # Terminal output in a text view: fixed-pitch font, a lone \r rewinding the line │ ├── error_text.py # error_text: a tool's English error (exception_tags) in the IDE language +│ ├── code_result_logs.py # Only warnings and errors from loggers reach the editor's Code Result panel │ ├── closing.py # may_close / AskingDock: tabs and docks with unsaved work are asked first │ ├── dialog/ # prthinker settings dialog │ └── syntax/ # Automation keyword highlighting definitions diff --git a/architecture.md b/architecture.md index 0c7feb83..c40033fc 100644 --- a/architecture.md +++ b/architecture.md @@ -144,6 +144,7 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() | `write_file_with_encoding` | `utils.file.save.save_file` | `menu/plugin_menu/build_run_with_menu.py` | | `DEFAULT_ENCODING`, `LINE_ENDING_LF` | `utils.encodings.text_codec` | `menu/plugin_menu/build_run_with_menu.py` | | `actually_color_dict` | `pyside_ui.main_ui.save_settings.user_color_setting_file` | `show_code_window/code_window.py`, `automation_menu/auto_control_menu/build_autocontrol_menu.py`, `tools_gui/diff_gui.py` (the diff's line colours: `diff_added_marker_color`, `diff_removed_marker_color`, `syntax_keyword_color`, `blame_annotation_color`) | + | `RedirectStdErr` | `utils.redirect_manager.redirect_manager_class` | `code_result_logs.py` (the handler `EditorMain` hooks onto every logger to show records in Code Result; PyBreeze raises its level to `WARNING`) | PyBreeze also relies on `EditorWidget`'s `current_file`, `code_edit`, `file_encoding`, `line_ending`, `mark_ignore_next_file_change()` and `mark_saved()`, on its private `_file_watcher`, diff --git a/architecture_explore.md b/architecture_explore.md index ce2c467f..56c7b727 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -68,6 +68,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - `update_language_dict()` 併入 PyBreeze 的 735 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` + - `show_only_warnings_in_code_result()`(`pybreeze_ui/code_result_logs.py`)— JEditor 剛把一個 `RedirectStdErr` 掛到當下每個 logger 上、收到的顯示在 Code Result;自動化套件 import 時把 root 設成 DEBUG,所以開檔就有 gitpython 的除錯訊息以紅字出現。把這個 handler 的門檻調到 WARNING,logger 本身的層級不動 - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示 - `add_menu_to_menubar()` — 建構全部選單(見 §5) @@ -457,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 131 個 `test_*.py`、2403 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 132 個 `test_*.py`、2405 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 4de748bd..5544e885 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -981,3 +981,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `images/tool_curl_import.png` and `images/tool_curl_import_action.png` redone at 900 × 720 with the same command and targets (pytest, APITestka JSON action); `architecture_explore.md` test count. The README text does not name the button. - **Files**: `pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_curl_import_gui.py`, `images/tool_curl_import.png`, `images/tool_curl_import_action.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-309 · 2026-09-24 · Keep library debug records out of Code Result · #fix #editor #logging + +- **What**: opening a file in the IDE put gitpython's debug lines into the editor's Code Result panel, in red as if something had failed: `sys.platform='win32', git_executable='git'` twice, and in a git project also `Popen(['git', 'cat-file', '--batch-check'], cwd=...)` and `Popen(['git', 'cat-file', '--batch'], ...)`, each twice. Seen while redoing the README's main window. Two things meet: je_api_testka, je_auto_control, je_web_runner and test_pioneer each set the root logger to DEBUG as they are imported, and `EditorMain.__init__` hooks one `RedirectStdErr` handler onto every logger that exists then (gitpython, urllib3, openai and langchain among them) and shows what it receives in Code Result. PyBreeze's own logger ("Pybreeze", DEBUG) is one of them: JEditor skips a list of names that still says "AutomationIDE". +- **Fix**: new `pybreeze_ui/code_result_logs.py`: `show_only_warnings_in_code_result()` sets the level of every `RedirectStdErr` on a logger to WARNING. `PyBreezeMainWindow.__init__` calls it right after `super().__init__`. Loggers keep their own levels, so the log files and other handlers still get every record; warnings and errors still reach the panel. +- **Tests**: new `test_code_result_logs.py` starts the real window in a child (`started_window.py`) and logs a library debug, a urllib3 debug, a PyBreeze info, a library warning and a PyBreeze error: the first three stayed out of the panel's queue only after the change. `test_jeditor_contract.py` pins `RedirectStdErr` at its module path. Measured in the IDE on a demo git project with a file opened: 6 lines in Code Result before, none after. +- **Result / numbers**: 2405 tests in 132 files. `ruff check` clean. +- **Docs**: `architecture.md` §6 (the new internal JEditor name), `CLAUDE.md` tree, `architecture_explore.md` (startup sequence, test count). +- **Files**: `pybreeze/pybreeze_ui/code_result_logs.py`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_code_result_logs.py`, `test/test_utils/test_jeditor_contract.py`, `architecture.md`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: upstream, recorded here and not changed from this repository: the four packages setting the root logger's level on import; JEditor's skip list naming "AutomationIDE", and its handler sitting on a logger and on its parent, so a warning from `git.util` shows twice. diff --git a/docs/updates/README.md b/docs/updates/README.md index 2d2133b1..624d1ed4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-309 | 2026-09-24 | Keep library debug records out of Code Result | #fix #editor #logging | [2026-09-b](2026-09-b.md) | | U-20260924-308 | 2026-09-24 | The cURL import button names the chosen target | #fix #tools #readme | [2026-09-b](2026-09-b.md) | | U-20260924-307 | 2026-09-24 | Redo the README screenshots of the AI tabs | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-306 | 2026-09-24 | Record the gitpython floor question as progress #109 | #docs #deps #security | [2026-09-b](2026-09-b.md) | @@ -390,4 +391,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 106 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 107 | diff --git a/pybreeze/pybreeze_ui/code_result_logs.py b/pybreeze/pybreeze_ui/code_result_logs.py new file mode 100644 index 00000000..e0bf25dc --- /dev/null +++ b/pybreeze/pybreeze_ui/code_result_logs.py @@ -0,0 +1,29 @@ +"""Only warnings and errors from loggers in the editor's Code Result panel. + +JEditor shows log records in the panel, in red, through a handler it hooks onto +every logger that exists when its window is built. The automation packages set +the root logger to DEBUG as they are imported, so every library's debug records +went there: opening a file filled it with gitpython's ``Popen([...])`` lines, +and PyBreeze's own debug and info records went there too. +""" +from __future__ import annotations + +import logging + +from je_editor.utils.redirect_manager.redirect_manager_class import RedirectStdErr + + +def show_only_warnings_in_code_result() -> None: + """Let JEditor's Code Result handler take only warnings and worse, on every logger it is on. + + Call it once the editor window is built: JEditor hooks the handler then. It + changes the handler's level only; loggers keep theirs, so their files and + other handlers still get every record. + """ + loggers = [logging.root, *( + logger for logger in list(logging.root.manager.loggerDict.values()) + if isinstance(logger, logging.Logger))] + for logger in loggers: + for handler in logger.handlers: + if isinstance(handler, RedirectStdErr): + handler.setLevel(logging.WARNING) diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index d8e5ab63..e009e126 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -15,6 +15,7 @@ from qt_material import apply_stylesheet from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.code_result_logs import show_only_warnings_in_code_result from pybreeze.pybreeze_ui.closing import AskingDock, may_close from pybreeze.pybreeze_ui.editor_main.file_tree_context_menu import setup_file_tree_context_menu from pybreeze.pybreeze_ui.gui_thread_gc import collect_garbage_on_gui_thread @@ -54,6 +55,7 @@ def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, # missing from it, and a menu given a None title crashes Qt. update_language_dict() super().__init__(debug_mode, show_system_tray_ray, extend=True) + show_only_warnings_in_code_result() # Note: EditorMain.__init__ already calls load_external_plugins() # which auto-discovers jeditor_plugins/ in the current working directory. # Third-party plugins placed there will be loaded automatically. diff --git a/test/test_utils/test_code_result_logs.py b/test/test_utils/test_code_result_logs.py new file mode 100644 index 00000000..a20f0562 --- /dev/null +++ b/test/test_utils/test_code_result_logs.py @@ -0,0 +1,41 @@ +"""Only warnings and errors from loggers reach the editor's Code Result panel. + +JEditor hooks a handler onto every logger that exists when the window is built +and shows what it receives in the panel, in red. The automation packages set the +root logger to DEBUG as they import, so opening a file there filled the panel +with gitpython's ``Popen(['git', 'cat-file', ...])`` lines, and PyBreeze's own +debug records went there too. +""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +from test_utils.started_window import run_started_window + +_EMIT_AND_READ = """ +import logging +from je_editor.utils.redirect_manager.redirect_manager_class import redirect_manager_instance +queue = redirect_manager_instance.std_err_queue +while not queue.empty(): + queue.get_nowait() +logging.getLogger("git.util").debug("library debug") +logging.getLogger("urllib3.connectionpool").debug("connection debug") +logging.getLogger("Pybreeze").info("own info") +logging.getLogger("git.util").warning("library warning") +logging.getLogger("Pybreeze").error("own error") +result = [] +while not queue.empty(): + result.append(queue.get_nowait()) +""" + + +def test_debug_and_info_records_stay_out_of_code_result(tmp_path): + shown = "\n".join(run_started_window(tmp_path, _EMIT_AND_READ)) + + assert "library debug" not in shown + assert "connection debug" not in shown + assert "own info" not in shown + assert "library warning" in shown + assert "own error" in shown diff --git a/test/test_utils/test_jeditor_contract.py b/test/test_utils/test_jeditor_contract.py index 55517971..b61e5b3b 100644 --- a/test/test_utils/test_jeditor_contract.py +++ b/test/test_utils/test_jeditor_contract.py @@ -44,6 +44,8 @@ "menu/plugin_menu/build_run_with_menu.py"), ("je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file", "actually_color_dict", "show_code_window/code_window.py, auto_control_menu/build_autocontrol_menu.py, tools_gui/diff_gui.py"), + ("je_editor.utils.redirect_manager.redirect_manager_class", "RedirectStdErr", + "code_result_logs.py"), ] # Names PyBreeze imports from je_editor's top level, i.e. from its __all__. From ae7741a4b1fb41f8293adcd25034e23500f11fd4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:01:37 +0800 Subject: [PATCH 041/166] Redo the README's main window and correct its caption --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/updates/2026-09-b.md | 8 ++++++++ docs/updates/README.md | 3 ++- images/main_window.png | Bin 99611 -> 91220 bytes progress.md | 1 + 7 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c23c6c75..a7c7f118 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ![PyBreeze main window](images/main_window.png) -*The main window: automation keywords highlighted in an APITestka action file, project tree on the left, run/format/debug/terminal panes below.* +*The main window: an APITestka action file open in the editor, project tree on the left, run/format/debug/terminal panes below.* --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 3675a631..1e38f988 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -11,7 +11,7 @@ ![PyBreeze 主窗口](../images/main_window.png) -*主窗口:APITestka 动作文件中的自动化关键字高亮显示,左侧是项目树,下方是运行/格式检查/调试/终端面板。* +*主窗口:编辑器打开着一个 APITestka 动作文件,左侧是项目树,下方是运行/格式检查/调试/终端面板。* --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index e5ac639f..7dfad8f2 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -11,7 +11,7 @@ ![PyBreeze 主視窗](../images/main_window.png) -*主視窗:APITestka 動作檔中的自動化關鍵字已高亮,左側是專案樹,下方是執行/格式檢查/除錯/終端機面板。* +*主視窗:編輯器開著一個 APITestka 動作檔,左側是專案樹,下方是執行/格式檢查/除錯/終端機面板。* --- diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 5544e885..6f7fd844 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -991,3 +991,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `architecture.md` §6 (the new internal JEditor name), `CLAUDE.md` tree, `architecture_explore.md` (startup sequence, test count). - **Files**: `pybreeze/pybreeze_ui/code_result_logs.py`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_code_result_logs.py`, `test/test_utils/test_jeditor_contract.py`, `architecture.md`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: upstream, recorded here and not changed from this repository: the four packages setting the root logger's level on import; JEditor's skip list naming "AutomationIDE", and its handler sitting on a logger and on its parent, so a warning from `git.util` shows twice. + +## U-20260924-310 · 2026-09-24 · Redo the README's main window and correct its caption · #docs #readme + +- **What**: `images/main_window.png` (2026-08-03) no longer matched the IDE, and its caption was wrong. It showed JEditor's Help menu, which `PyBreezeMainWindow` removes; the automation keywords of an APITestka action file in colour, which the IDE does not do today (#103: `.json` gets JEditor's generic highlighter); and the project tree of this repository, including a folder named after a development tool. The caption, in all three READMEs, said the keywords are highlighted. +- **Done**: redone at 1600 × 950 under dark_amber from the real `PyBreezeMainWindow`, started in a throwaway git project (`D:\demo\orders_api`, branch `main`) with `USERPROFILE` at a throwaway home and the same action file opened through `open_an_file`; both folders removed afterwards. The caption now says an APITestka action file is open. The tree also shows the four `.log` files the automation packages write into the working directory as they are imported, as a user sees them. +- **Found on the way**: the gitpython lines in Code Result (fixed, U-20260924-309), and a tab opened from the tree showing "*" before anything was typed, recorded as #110 (JEditor). +- **Files**: `images/main_window.png`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `progress.md` +- **Open items**: #103, #110. diff --git a/docs/updates/README.md b/docs/updates/README.md index 624d1ed4..ce597b4c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-310 | 2026-09-24 | Redo the README's main window and correct its caption | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-309 | 2026-09-24 | Keep library debug records out of Code Result | #fix #editor #logging | [2026-09-b](2026-09-b.md) | | U-20260924-308 | 2026-09-24 | The cURL import button names the chosen target | #fix #tools #readme | [2026-09-b](2026-09-b.md) | | U-20260924-307 | 2026-09-24 | Redo the README screenshots of the AI tabs | #docs #readme | [2026-09-b](2026-09-b.md) | @@ -391,4 +392,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 107 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 108 | diff --git a/images/main_window.png b/images/main_window.png index c8764d3541a1a2b3618dc880106b621ca94075ca..13d261695541eb066b07d442020e277ca69f9d76 100644 GIT binary patch literal 91220 zcmce7WmH_d;9i^%)gNj6e1ONa~rA~DnwAgPze)_L#kh?$;h)n;!6LPe(z!vi5KMy|> z(A59uDAA-32KRp(*eH4ohX=Sv5^ghynYVWFR`yvHF1 z`b~nTjhHTTw`I^^;o#2JgL+ibw+r+{C%tVBkVn(&=(6U9M-;ADwgc3v?TDnb|=rurb0;` z{`uSZ#f1z_(9_EeA@2xNupwW3hxeJgxP}bqt#{euSlDoOewQ?5ivoqOCmo|czOUh9n1R6ZQ>&!@Pwmfa;ciT5$!sTsBb$NcsMWRu9^_*CJuy`M=Ku_XZ z@>7dbXtJ!#DqFtRw>dd<$43zuD80k}iJ6us#W3h)&noW#pBrD9I@~tlhdue5GwlT@o1w7Bb>Z zrd@c&8Vd2_BJK83{#>6(`A25`8qF}cfD=*YDV5rHJF=P#TBrIznxLz>>3-ynn}akP zjt44yxn$dc*(8(wPN-`+HlGh+p}5S?Mn&_nZ(E}uBe#JBO7`~lAlj$-DxH>-_qrk| zmdgpETu$JVFmzFH%UiI1jpK6h#S~udf-N^>X*|06$#|W@dZ&tAit0d*jSk22VY$Ri za>;euzC9|+uz}tOZOTe>mD8(l^d%y(mH4-(qN<#w6C)N1QgZ%} zOXLsf^fH6kBPD(@@s9#TpuORSjmhUfLQ#JeSrbJ{`P1&`O@H~x zg;3Dm$}sRnBRo2>Z+U1` zOx)pg+YIk|V2i^O8lZmVbmhl+qRA6;On;-@U9Q}^|5Kk=ilC?W^SQ%H05Rqz_T*It zi^;3&)&e{l(X+*#3LcBGeyepuT^NJK?nR~qN1FUxL*F)NBKK))YQMwd_-G%Ii8OHV zU*`H+t6yWC@6gIP8z%q^IU(@ znGlWc8qmU?ipOt*1CQfQ4S4ee1I(W0$MfP}bc=rP7;X^u8H3zk70-=@V+dm9OCC2~ z*q=PlAxpkfih7*uUsz{{HQMOZ#edS%y;i0|Wb{0E4iWV@f5lWfE*2IP01@%Q{c=PL zGP1*JYoO8uRFw|Plf&7Gp<9~Xl}O+g72wyvujjVUrrjaCEA~jsWsbJ3n(^_WxQ+gY zTQ_#vyG(7aOVhBRaYo+&@1v9kQiqlH!?9s=D`k==x8a|I`pwQt>I~TDXhD8&V57fh zeFmC0Ha?wyX$TmcFzUGLyxC(*x9D|v!F#H|OuO_`Y{Q+G zMHj(;`VBvrKqSx{bUuX_Va@e@Qsjf**1_hQV2kMQmV_+Z;f})Wb@t8xxU1}}Ic~e< z&xc!I%Hm0*Aj9hSHT7sXsl*jS>yWkXB6(4o)DGg|1G^>`}065V%3CLB@%O0Ck z_)fADwf|dR>PpM|?nl>9=^`~c>=fDnkBhC6Jge8~BCmD3g}OTZu2M_IobT)&JWfjx z<@nBPH{z20B%Y64nG$-tu_I^Fb-gtemC|jqTsS9q9*2U8iim{d%IvTa99Se1f4|c- z$!q^yRo<(Za~Y68vKEnmztm}w)poV7@_00Al}kdULU4DuxF3l^!hiIV?yx-hjaPK& z{!?6Tb2Bm?>!nbh6Y`kWY195#T!oP!3yXHHev$9Z>7*4m&VI8WzAi*QCKr z>p%LjIBqDFd@eCxZG?x1XHewp(WcmsghRmP?LNNjL0z6HAt@uJl*3cxGs;u5A`*DH zQ(K%(J>Jt?#`{h@IDq|n10{u!A=t(72(%cPR+qYN zKGLs5>EfpCZ@bZxR5aM^xL~?jNUf%(My;2WRpJBW@pV7;y*+6&XmNB}ZRTfSXcP~V zLC&L|^ci>=-gzJo8^=}1x+M(*4+|-X9IC74tON z%fT1s!?Hf7RjFwM&R&v*|U{Sm#@c_V`|n9o%Ge&vZu=Q-Hrx}Zp^

    Yc;LH9k1VFd{||4HmO)9Y}2Szq}9;y<}bhRx2> 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 NRMxzguVnG+zX8KraLE7w literal 99611 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 NYldb9=0.1.34`? - **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. - **#109** [DECIDE] `je-editor>=1.0.27` (`requirements.txt`, `pyproject.toml`, `dev.toml`) lets an environment keep a gitpython below 3.1.59, which has CVE-2026-78676 (a config write turns a quoted value into a live `core.hooksPath`: code runs on the next hook) and CVE-2026-78679; the IDE's git features use it (JEditor's `je_editor/git_client/`). je-editor 1.0.28 differs from 1.0.27 only in requiring `gitpython>=3.1.59`, and 3.1.59 still has CVE-2026-87818 (the diff API's `--no-index` reads any path), fixed in 3.1.60. Should the dependencies say `je-editor>=1.0.28`, declare `gitpython>=3.1.60`, or both? JEditor's own floor is its to raise (`D:\Codes\JEDITOR\pyproject.toml:18`). +- **#110** [BLOCKED] A file opened from the project tree shows as unsaved (" *" on its tab, `_is_modified` true) before anything is typed: JEditor's `EditorWidget.open_an_file` fills the editor with `setPlainText`, which fires `_on_text_changed`, and never clears the mark afterwards (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:295` and `:347-358` in `D:\Codes\JEDITOR` at 1bf81e3; plain `EditorMain` does the same). Needs `open_an_file` to clear the mark (`mark_saved()`) once the file is loaded. From 855e0a9d41f3254969f73770700394c9ce0b3cbe Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:05:50 +0800 Subject: [PATCH 042/166] Remove the images nothing shows --- docs/updates/2026-09-b.md | 7 +++++++ docs/updates/README.md | 3 ++- images/main_gui.png | Bin 52721 -> 0 bytes images/tool_hash.png | Bin 28602 -> 0 bytes images/tool_http_status.png | Bin 33112 -> 0 bytes images/tool_json_format.png | Bin 24263 -> 0 bytes images/tool_jwt_decoder.png | Bin 36857 -> 0 bytes images/tool_query_json.png | Bin 22060 -> 0 bytes images/tool_regex.png | Bin 35148 -> 0 bytes images/tool_timestamp.png | Bin 21441 -> 0 bytes images/tool_url_builder.png | Bin 25983 -> 0 bytes 11 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 images/main_gui.png delete mode 100644 images/tool_hash.png delete mode 100644 images/tool_http_status.png delete mode 100644 images/tool_json_format.png delete mode 100644 images/tool_jwt_decoder.png delete mode 100644 images/tool_query_json.png delete mode 100644 images/tool_regex.png delete mode 100644 images/tool_timestamp.png delete mode 100644 images/tool_url_builder.png diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 6f7fd844..7868b906 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -999,3 +999,10 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Found on the way**: the gitpython lines in Code Result (fixed, U-20260924-309), and a tab opened from the tree showing "*" before anything was typed, recorded as #110 (JEditor). - **Files**: `images/main_window.png`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `progress.md` - **Open items**: #103, #110. + +## U-20260924-311 · 2026-09-24 · Remove the images nothing shows · #docs #cleanup + +- **What**: nine files in `images/` were referenced by no file in the repository (outside `docs/updates/`) and by no Markdown file in the workspace: `main_gui.png` (a 1918 × 1030 main window from 2026-04, older than `main_window.png`) and the single-tool pictures `tool_hash.png`, `tool_http_status.png`, `tool_json_format.png`, `tool_jwt_decoder.png`, `tool_query_json.png`, `tool_regex.png`, `tool_timestamp.png` and `tool_url_builder.png`, whose content the README shows through `tools_montage_a.png` and `tools_montage_b.png`. Two of them were redone for nothing in U-20260924-298. +- **Done**: deleted. The README screenshots left are exactly the ones the three READMEs show (20 files). +- **Files**: the nine images +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index ce597b4c..f41796a5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-311 | 2026-09-24 | Remove the images nothing shows | #docs #cleanup | [2026-09-b](2026-09-b.md) | | U-20260924-310 | 2026-09-24 | Redo the README's main window and correct its caption | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-309 | 2026-09-24 | Keep library debug records out of Code Result | #fix #editor #logging | [2026-09-b](2026-09-b.md) | | U-20260924-308 | 2026-09-24 | The cURL import button names the chosen target | #fix #tools #readme | [2026-09-b](2026-09-b.md) | @@ -392,4 +393,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 108 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 109 | diff --git a/images/main_gui.png b/images/main_gui.png deleted file mode 100644 index 922aad560c26906addf6e0682b066210476ac840..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52721 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 diff --git a/images/tool_hash.png b/images/tool_hash.png deleted file mode 100644 index 692a9a4d2920085d2304774e727fbd8b2429b476..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28602 zcmeFZcRZY3+cqkVgdie_9=#Jri5}63E+(QyjNW@EBBJ-+1yQ0*qK!Ig5JoSf*BISk z^tvzi{XFk}pZ9ydcYpia|Lx!RM}CW$Ys|W?bDifp&f_>%Le*8}@o_0|v9Pf46%}MO zv9NBRU}4=t+`kRH!XvF52MiA!6m*@jun0PC{@qICAfUv;dWNMaBc<(;u{GlYQ zvwJMO%I&;5H@Vs$!({z<68R`mCakJ3pJa?s_JAprJ`@_`o4nQbOt*rd2hIplOp4$w zNv%5OT z5=IeV$kA1B0fv{AF>Ao^(Tr~B7BKd`%Dn>&X+bhLz`&0eN&pNNq>rhAVUY9ResFdu zRQ1EFwt!htdU{USiUV+E?ezMq@uwgwnUY|=6Ot%u$-VPayfK&OW}_h{U22FF0U-z- za9K+qiOA6Ui2X-24{_J!@xmfQw<_@Rf}O@iba_t_7s#}&)?nIC_nK7YuaX*9x_(LW+YV2FPZUam!JG!Q*3&$dg=%o% zf|tSReW*xl@S6sx)lZh2=inaxfmuW()2?mennq&(e0f{MN78>W#a=F@y&Y+Lva&|o zRo{H>R(<|mVirC-Xv{$4w-s789nYeYd8Gq8#*Okl`B`%>2s_Yt>4djS_2RTcYryC5 zun10jXf<5))yQb2H`%+`8R?hpbG9>WpTOpN@q4Sm4NJ9O?hYp5O<^PVY}!_tz{Z*pd_ zn1zf>x1fN!MBqwODU3A??r(Upnr%iXuvfwmzjYqY8v5y(hgcTQ_xbBSuu%dJxc~k0 zM??dA+{v4lhP1BR`x~P6uHycSOA~v083F`<|i*NK`Tlj#asM!Is>>bGN7|5QF*KEHo);3yNrDYBbVeXuy z%D-?)zWDEX`uI?FIR;8z4pgit-oYXCU0EUX6}k|i>aQ@}*)#BBQ&+D=w@5Y2UKt*{ zBg~vLr>e93%SU?Nlmxol3I>PgCYV?1a-ztxwq;}*TavjAtd0(TyKasOQat2N?%muw z;bSQv?W5VJBm{2PLX(%6I=T1IpVseUAy8nvOqbVnskXWQDl;ody25_3rCDbpk4kgQ|1P>}h;|pyklc*Xu z^ZcpKjazfkaO!G{IV!`2Sv&Xjjtyq+VWqBGBB~tas3e059 z?_Pp)7x+VuWZu>IoIEL0B6X)Slf3u!$KZU#OrxM=dGyD-H|1=HicbU0yA;)ai`I6X z_$*8{m{d$YA&t##Bi)k;Bmu;bjz>+OSZb?F%ezwO@KvP z6JHl=&A&bql6}DKlDpi%gQI(sYBO{`mZ52S!FzgO;J5z|rIjgmt<>n!Q>@d)}e14bEfH;@ktX9;kPR3(h_dtqCRpvL!hiUa)_}Ow%oN}C&=;VHurW> zj&5E6<+b9$X0~aj2%5`KYc^Y`OWt|g+jQ;jhqAXJx1Mm%JZH1ph(4kf5?-4RN-|aI zO%g;qMrE1z_V@Wa*tF8@p0cReoT_^5VFPRNdRd8eh%VQzy+R1>`rtM=FYftbyhXY} zR^=2;zM03<tEIQbRE z-7&~|Dsz8drQf^sRYEGp*KhP1zxV3igE5Yco@7$^H6QAj=|M?Xr+~*xbP1HxXU}nD zJ-_3Vq-12YiB9EbOmly=4%CBdRkXHcs{ZF*UVqXrqplMrLmQJR5plnHA8TkbS3o^S zsh8|*5AV;eB&O2rNAtPmu^~HLTp6lL{1p3=x6f_Mt95MyjpRwtLw)CeeIq6FuTu#| z_Vj1KHCua)o?WjuzXpvP^5{)TJV_E8@q#$E!F==XX3c^Z&F*EXvM|ko4ZCdS$tL=@ zU+Wbb2sPa1=<1LdZkae1=tZ&Z$(*X3a>=JNcSgo9`x(XkfIuR6B6_f}>3dzcWw#`` z<&)M=yLRiI0t*$wl(}~QHn=Ib+%@k_zoj45TSM$n?D@yAr z+fS%yXgax`(t!NS@~%oO!-YgT55phdJ>rrG66K?)os`at&N>qG25na=)@8$qhZ>_= z!F@S}ZcTh%k@Zs;Qw(023bjO!#k^>xqsnD`Uweu#TbG)Q)RcctZn-box{^bz&HN0) zOzRSPC4nBIxk8Tl^u{J}MWboOUEUeCb6a{xdv+d0nqfbGp^*?zSf3`gmdyhs;7m@O zvP%Jem_%lRhfT3-PvMZYyRdm_mF?UnU;2s2*a0&GvbMl)Th)9qEtvFOwm*X3N!3mY z&!`V^xg1fbDj*GcA>{Y94;2$%BQjF?O-|sUiWPZc5Ipa00OhohQ@efn*mP#VciXvT z_LITSmZLLb>C$t)qnWV`;KpFWVQM1J#si=0Qqy!Qg5*Y@N`_$sQ|Ff`jCA!w)N{Lo z#u=*g#`B`{V1c9L?C$0^Uh_n8B(tsLLVqng*IV;}o>*@EBbpbBWpCxw=fN>Ymu6&6 zMH29p?n-4EsD4xKBjcq`t{+XJ`ftqg60(@c;5=P093OugTMJIZbfEk&`9`ZQJ|1GS zpV_vW9(_vt>pGJ@t8R@;e@K z&V$Qu$Gxbc`&Yf>lL1k0KqsFV6&{;rw)~R4XWg}&Xst68iKcz?wQAwKGz`2as`udM z#V?NB6n{zXOf?cc!RIGqM6#31eYD}MzeTdN{+>VVWHaS#3}5EJpP>@Djq8yYfo3gK z_FKkm*$sU0-~DuoezLbY>A9*E<}9T4ipt0V#J5NTK-XGDCGH{Pch!BoRo*iBK|Hk? zS}ohL$ZaS5b#CPM-1DYE9z%ov7;Zx%y(aHj7B;R8io_t919dKGWMO;ucJgAw`*yLK zr=MV2bs4*3II*+A=4jC!-nF%sPj~6}y+T7iw^)uzT57I)NIe3UQ0Us6W?SNGw)fjP zzO(Po*(L4wL&H(qs>SEKHUWjmsbIBB{(8ezjt^@AM)C{mi(X&uNs5NK0qOaY6?W?F z;zr{X9K5FnyRVnNb-mmmHQ?&i@x*j7>e_b}EsGXMG3OE*Rm-HD_N;v7^`$5N;9+L$ z!+$*w*N`q;$*OyXEu0iWWBH!5R%N1VC!vRF?E*J2`s;!}7qej;8&Ei{B| z&FM>udGeG1Uifnivq^CY%tLh>XI=wJws_L(%-?@8JdUk^bUqQQR77UO|$TpG-}qOVx35QLtT?!i}E zUna?%FMe4|<}(yFwwt3O&seSI3CF?Hsa}50W5HMXTf?O7$;H zC%LYzB!HY6j2KZ2mC=S;huD+AGr=w{x5r9L?dRK4sz|bRf%jYb+(qr>cJ?72P%wgF zbxLzM+ylc7yzvnifChKZW~`2^&bP4C0)UsWY8^eign{wD@6Y}%fcjr)?EcRO|C_G& z|2&-ke>|KZRhQukQ+lNkS*4p6!u_t%yCr3fPk5PdDhXMcDxeeSb_HHvs8J4C0bd9u zU8!h%8VM@>$*${D`jwkkXF_1wYdP?SGpTjt)6lGVQjw-!|N3-6puceVXJtOpk;sfM z*ip#R1Goa~7l1mh&qO2jIJd>9M1~eO1g&&#o7_NsR?pgPY+(HF8JAc7(F8;S&^}iS zy3ul_af^$;(tk$Pa&muBtBPHB#_`305JT|Q;XbPR)Y11Y<4xD)uo@HU6;0&+`HVet z5bPpSwIG~Z#d%symzx%>1iX(mBQkEW6HNC>;LGOVC&JXV&v5xdw>R|kkek}#z@pHb zWJF0n+{~ur@RgW)-dDKW33z>hU{P6lbVej%zW!$Z{~Nr~zh)50+1D&A2k_v7(XzZ< zZD(%gPYn*ThE*Ex-~!jW?qUP3mc5ea_miKte}i-@3n}6WDOJ3F)8nN4Wi!O0$I0?t z+**mgV65I@WK;<1lCx}%?v(veEaT8OeqqaCG4IHUA0k3^&34<|mV?@{AxpUy%O01h z{(s1@y$T5j$C~G_l_#UGnEZLQI4!=vY!?#Gm617Rm~?)MUAPkCQ8S>s^wP#1o=WwHpq_tEFs;%; zBm#}&1E9x^e0o+sSkC6Zl+n29UVL>qlOuF5FF>zWiZS;V6f+>+maX;;-#fqlc#pQ3 zJs~WQ7Z2JLD>)g#qu??nbl5!TEi64(D?k3g2_tfWGq8tEO??7-|M-V-w96K!ybQEz zG1OuaJ2XU-?@yZwiFQ&bgXSTXxHbQ=x=L0^mgR3UMiC7&yD6;9-wM|5imI>+h?bp? z(bwKblN?_x`s|+)nfXm&Nlo^LI(u}|wipnG5_}}pt_`+ECP;c3`q_qn<5=l4#-m4? z=A!g!1A5-B*U6tGJUm^I${pcKR~hb!t?#!zsk6q61a0ZFXDblup!_5mh&395PfDr6 zLL0|VPPNb6VijaCpV?0T-DtF$Yj`-F_S?51_V#K%VZgtH1Olm7s} zQY6+&Ud4qLIq7oKXSmTM1tT7XTA+F1?d|$?nlqFBuNYbV+Ka%E<~4ybI4|!2RHlX0 zHais&5P~E|`dod{!>4R%5giz9D21Ag-cheA?6r(waN}jnXfcx!+HWrT(S?_`SH1)i zPJVq(zSr8Le=741{>G~&@!FL-U;xd8vD%xzTFxt$4!=ku%uMJn{4Tz`PTDxab?yH3 z#aC##3Eow7vkd(iCLi@&5Va-Hj9u>Jlh0hR?85PhzY`>P?W zU2a_Wgq=jL(Hf*tAr(&LZ~A+f=)-FMTx%s+Vhm41J7?BTN8#jXhoSQM6>~jHhs9@N-1iBuoJcd(nz*xBXEcmmcb0d!e%T5?yLQutr?J3n z+b-Sd>SiF$!+J)Z=S@AZ(q7nic`)YB5aw65+wWFAEdrBYf@}(+K+Z|$-pRS=R`^;b z7G=!@ClEL^pShWY<(GAIO z>S7bn+Bv46rIspj@7~iuuJHCyT9-qJi4xTi*XDIHQ6b#sUsUmC43+Tpw_mYG8pX{I zbwP3Aql_~OWt4j|{09ccYYnH##v(Nf$ejeB2pHM+4(Ky+&9h(rP)%UGS_TK5jh)!y ze0y2O8%$J`Da{h@EJ}nmqj9qoTZ{4bYXUWU>o1N2U0M2nAEH+yu+tj#8FT%IE!LUW zgSt+FjBtU;x|ZyXTfF-rRUgMBpC(MdBpk2yiAGvXH}OTCQT?A)OSa~IF(V6F3PKsm)0&{i|o&Q6bk86z%;+$ zS00h*c1?He6}vg2TVxneDZKu>K#Yi5SnH2*>1B1wHdagKO}5KDOkup7$2*WQfh+0m z_d&_)3zaOzG*7zq5v-RLo(PCS1hQW$#$61- zmadyxgnVO4^dkF>3igzLMK(y!x9EvPdFNeLb8rwR2f-o3Kkuo@EGLCDME!|=ov;>p zkv6Bt3l`R8XCTZgr zv>n{bK%9w>2H1K&4r-%VJnr7?-v9eRSZ7U!g-ef_-r6Mus(r&1vXaBAI+Anml8Fon z`-Fw81=HPjxO62S%<2Ixcb4lUsrOvifllks(;E~2;g=xX-r!YWS3HM9UDs_c2KipWlTdEX{i zJ*NhOfwEIegTwO@qFB|!^yf#-!O<629t{NZ14b`U=|reqhssGYrxUXIh^wPm!EHhs z5xx%-kLbMsHAWA{qLwCdS!Rdk<(ROk;gOHHIG;I9e{KTY?n$;rMtUrWuacgD0WW#9 z%s{yisgf)w;x69pGBYh<#1E>EvJ?_qGEE}+TSocHdPmDM-4@89(tim-u?Q( ztClK}=RU7?K|!c({I?WRDh%MImg}M=MiCurA5~S=#kh&uiK;4o#rhq~_5Tn@{N5i& z-Dz;wF4yJ-v=yhiIrO1Mrbx98x4{eVvxMQ$P`}9FQecu;IhwKlApJ+5gVXp8kCwyQ zf-Ei9_Z;`c2FUtdHKF+iN{XHvsbsjwh{#aK7a6p^7qn8341KO9dGng>#_BU!!!~Xf z9afR{XATju0dT4M;^v}ssKv^6fuQTn-B<=1=<2qRkx+Wyjnek=&`QQ-VS$d`3py=I z8vgA=5N><(iI{{$xy!mHpqrKJ71j*Bcc{}hjP6RN6#h%#;C=cqfW6A%D5mv&Z7Ap_ zxB9faWzA>&n`0T-#2FYoyUK;;eFt_No?IEhrbi0rKT6#>YrcB7+H`(obg}xFs7}B9 z`h0i!%2!e!W)?JSX4Gio>E$Ko<<+@n-{mh1QTNfmm|yKbqLH)^&XRMv|LG$aueiOV3`Z z_vV6M&Ldf_0@hQ1P=o^bczE86b*g}76-+=|r^|JdGfxIoykC#iK8TmxIF$MpM$&)f zTdsB;K%SZ^RALIe|8c3#lGflRt7k0jrsQu#0W5cle=DE&g^LpO(sBGxkUdq>CorC`Y@sw>7K2jX6Vb!I#yIla8}$BB zkDV^bXm5U^uHE%dprcE zExqhov6|zD)SmPp>dvR^Z;Q>$*#$ zD^aqW+cKk#<&OO^YoB(Qxqe2bX3NV6aBO+8KVg_S5^OrwHzyE9ZL?=HrE>6E9W;4V?;X_~GoQmfnv1haqRK=27u=4v0DsZ*pLrZAzdy4NZ~z zp?Q@T`||G$jT}qT(0Qkm%u$5;^YcSd^(OkVXDtSWjE||CqSDVz5ro#;;?2JaKu(uN zFa#CV>|)+S;nCmSm>K7?_6!=UWA%wJE&`yLUMwUGqu>*8zl4@E6H+h}Mo4%~IUht6 zYE!`g!undy@Q`|~H;kq)*Scd^C}F~mB;yzKQlGZ8jB1zrIi#MHe>tUoOAt zXYv@t4NktziSMkiVa+G1iEWFx5HotbRp}tJ^?jS*5YPC*YM~E>xS65hu;8k-TAG;; z>7_-q3rt?;h^DB3&qaeswl*p6p+Zh8j-^bWo%xOsGs@{XEb?p&a%7kfddGCQ2kBSP zwQwg{3~KALA0d?}W@`x@po*9e*eEt?&8>Opz`_~|SRdYcTJtx%1h%7MKo#GJD)(g+ zxv62v8_0rp?dRo zIFNK;E+|9p28pY(d?$@T+z-DGGbWJ{@E*h~lt)5PK*30j`B}>Y;lh5SW5aKp7X|K+ z=+!>zcWHo3l@zvmr&>-1EoL$&AnJ#tDnX~Y0SR%1TOS&3>*;1W?ROkNoFz~3G_=Ua za`BwUv8{qiKhW*`_8Xx+b{#C%U8(R7+8~DIlEB{P~ zH#9|j_hsKneQeNBWE5G|`qs`O9*-jjv$J^!a+>rU2Kn;wd<(h?*!m`DEV=DaKD+*1 zPsOM>A}S<~u)ZF@_j)VOMa-zP2Q9I|oZIpA>w4gsFQ&{J4kPgCrQd}NIs@7eN8-?S zn#m;~7$P~$=zA)avsDqo-s|<{_p;=U!YD+zgpje<@q?4fb9>?YA>BEvBH|zDa{(ET znL!AHNm23Ek8CK2%!n5%P}3$aY2kzbHR-8^2*TQ+r|oLlsel{CWb3T6p5i5JfICV- zxyMu3dk@4#b<`QOfNO5;DUwI&(TkgmVjZ0Ps;_d`yXdY7l5X8X zqF)|LsW*SRTE?nXPUw&a$1yewvTkliE#8@r_|dsf>F4T;@ilB6P#eLP1x1HNF6jvo zY>o;saL7-2c3&1hAJ*qRkgjI53;g_jl&#Uhv$;HJXCRG9aRig6`o6#1|sDnJ`rRAQil#H4S%{Axl|jjm3Z`PIpbw;c123}j=(*$-5Uro}In5r8Lf^;A-FH}J{hA(2&F$Itc=|Jw z;JmMYI6TY_BURAFL978Yp7ax9IKR4@F)?1{@s7?ud_1(V9Ti|N8Y% z+{6c}(6hULJNAR6j-4xea6a5D#`WQ!OEONpagnWlYx@l=QhE;g4e_&$92otXTj+@? z3^Po!r&Q3fm8l#5*1J7Nk9=G6MI`ym6Z^gT^UP z$+KC;vG$k}e;y&*_XajGwXoSNe6D;>-Z6LykxPu@pVySBHzMp`Qes%M3=w0(;j1q^vjgLd}TAYyl+6pNj+en z@}&8?_B>vQ6ET-4z}6zDUQXm}eQ#>4r?t>b>A_F20p~cE?K0I%J9B0cgkWl~MqY(# zovqwJWwE}B*+i7lKOpczB|^3&#JuMnqow3=NEz>1#gnU{A|Sdtr2U4zvnReK z#eTxyU6o8#o1dfJUsOE$tMn@N6lkez(VIwLY81J5#&_=rgO#TYuqq<03tE6i%&z*E1WCW<*`^3h- zaSUjnjUtl*3w^&YZfdoY;0c_mYZz3#j^EX@yNxH@NIM}M|Ec0m-iPMrFv{rWJv2&vJf zV=Q;)#@V84+dc#`MCseO$ql9~y4I5eJJVxlqEvxY>U%v-Aw}oTs6@RC@=49^LEfNX z#~T=U$!|8~CGS=9B;UO`4Kc`z45_P5xn)e?ckLLBR7N*FvU{)!+{wZ0N2nhRWZq{J zUq^+o&GQpTZ2}SA#7Z?hG#S!U-Alw=?%d!&zZ68@0VL2%^I#`|F&_CC6}@K)&8=kN z7SJT;@j2LEp-1NO*OgqgGR+u*iyh4XsShJPRv#?Og`+ zEw3&0x0WL#0f-f{_8Vmk-`84g+V(Q@5#cqrhz|Kf?&wBVHnWj(T^S$p<#iwRu=t1N zKOX4@X;q^&w$0Oz`~7D@`;m0I2->%J7n$@&G``gX)=Bo~xXHu6TD6?6UM$n)!h+54 zw~2-}O_i1EPv$~O00{Xq2(ibCw>eX2u3G5fXjXHC8syY1*wVKgNqDhT>xgFypSF>t z@^zg!Cjvxuqy9(de%@opw=SloD!KCG6Why`D?PhFG{pd)Liu3dX~j~PJD`DlNq z6S#@AFquX|s+5T)w@MCOF|^pQ*yCzFIC9(DAlx~Ylztd-DTwqsimP?WGucUdKmWEJ z=wUJizL?x(5!)S2wBH@)U2%91%@C+Uj|J1;zVnPyV$crkJebf9dz7murBEs_X*g&6Ks8WlfDbP;Urs!^Ktp<=7BiPwA$JxlCLz$@3iE*BGUWgO6ohsq*s=pY6 zgbNhCyLXu-DaHX40XFzv31n*cT+hl~1Q0p-5# z0YSh4W=2Co`3q)8HC}8lrxR6$i8=q(rD8Pjn<>{oUf=wBuUyq_PVe2Z!F4xY@o;1I zCvp33$RB2($KsZumrnIEGVnr0fHrEm{`9z>d~z|4h4ZKQ6vJiWE!|jByCLw9m%){a z$m|zWGO;$;xH1@*z;oy44@`0s+mw^~ubQgsTJO1c^5ON^ zI>D#^8Rpn69sswq5T*S%v|o#>4ZfM}KZ#l+SIg!V28?#>i4Pp3`vI?9V$YgS^E-jF zydt2AyP7Z%9d24FhWw_jVbqLCPrH}Zc^s}Zkc6%FyFnNP#NlIu60oR(RlVMg3pHLw z2oEE4+fKpLY2{OYFv)?ltRp|ZeDz_QmA20((cjWN3twIlWTyYFse^=tGqoYW+$@b3 zGH>^#j^s7O1|rX%4{ygxl4mn0BJCDE^8QQ6- z-T3s1oU`H5%EUzClVaE45w*-?_^#qOQk-G_Y23=W%uSM1MV3q#>F!eCpf*B_l)6SA zli&EJUs@E#KMpN21gq^@{jaDAv;8t%IjitZT3?9cE0`*3`b9*X_CHLz+@U=CQXYX% z<^sa_`=5oM1DOgKH~n1g#O}K4Nw!jzw%1WlL$>j=psFkaV1`EP(-~UH)=)BR&^ZM7OVFGO1z4=mB z=f7}Jbo7S;HyY~f4?=ot?O}udN6mT=y{61dS&4fGB6f!p6A3PReRPun8~jk~4A7q` zQ1jnRSQjURAoMp+x@4n_M%|5l*x*z*(U8Q#PER5k-n$Wq*119m7gQiZa6Io8~Fs zD|SJvoa8i0e>NQgxy$K6{H!*_Tq>x()m`pTn|tT>n0LcdG!d+OyD``KpQMoDla;?2 zC0)g%F9yT#ta#BHd+k6nM#IayobV)5d>;GZRH#z5qUTENx#vn4KzG0ftO=JQ}E2JFKYF)(@#m2wQfe zbrsr*L!E;>Q@PaXoE2UNwNvI=(Iq+f0Y1|3anUq*EZh=m-Dy6AEiNxZs59h$ZHg$D z-eK`JJL&)ZQVK8Efi;Di7mxVE$$DTe>Jnl<%CsH6?kQa_KU!J}mdvA9iB}YMM@5X% z8s^U>Ql^h`m8Xep2vw$yCrF!SAzKRv9u}v)n16O?o-|xGkt<8_*f|_~5*EaZWF3r1 zY0x-T`gUddS=*-UC69p6iDoHP#2M@>rDRa*SM~n2&Ux}Nt-9I`O3qI24ld1Cp{l3D zZO2BP7jQ=7!--mK!nM#fz@jIfCFE{lDFW{-$GSIx_-SZtj|4EBLmR6kh1x-rW0 z8POYPk)8%7z;L@URgB{D-ZYNN$}HEI7l4(X)~U>#I5 zOcL_0Zd&+>3HVgN_XHrHg2vd_jxQE<>jeBA2lNkgxLNL|5rG zjQ8}&nM+)T*x9TsjsFYt#GF-kD_H80%|OBoCAazS0Y9UL+fNygLAj+JfOUdxqxQU?miy*OBPg%Y5f-)Ch)$e8Hqh1c#yEeH@Vf8Lt04)z^(%ycRLYHB5cww9+E@15QZQ6S4i$P%P+t5V+(gw}PX1Ocn(zlPA^F0P?fN`uhg61zK* z^K*c05A4i%R_Sgwm;yww1K`mzi{S-lzf8+DVrF+>4lr3DfB-+p&|bHP2OJ5?s2WSn z;u84Y3V$*UEm@hU_#E3r|7D=N$tpe-N3g^shRJioDZP)*er}UOkO6rd(+@C1^%5jk z=W(BfrA<1y5Xy?pq(cD5TV1kFYX8u8h!fKH{0@mBy9Rw;79M}=ah6avd||e+>zZL~ zox{ehu<T%`gNS|CS1<=Sz0Ng+Q?$TvsncnGxrnbScy?Ao|bVq#py487p~;}t3d z!ODj4Ng_+sW0O#*ST)fDG=sZJc!g^fb*$G;T{9BXWuf z?^QCG8Q+SJP2W{K$Y(kN+V&V-j`k8xh+BsZXmBPG^x-DQVl3HlXdx!&{6*zRJbuSw zF2M9YwS0!rZ&|H973iMZ88=mCHK&=~QgQ!HrxESUnegI`tKWAFs>-0$mq>`39( zmrWdjom-j045*k*Kbsr9uN}@e z8m~W@w!RCVB|pY%b3-t!4=I^~~ z{>Fq~JS8WAsv)vh_208dcrr2!t0-ZE{aCFzKu+k} zTmy(q_P_#(zX~7x3|stWZhMtkmK-(#W`AH%+_t}CQt4wUC43AbYw98QY^>nR3V20k zk>GrjNI1to#~ZUZTR)3AG3(Q3W;S~t z*qC-|t6|o+><{{q&y14#(lKMw)$f%M1dvn#BAVr~PI3WBDW^(rMw5w{o3(0pmGPaA z`N)#%!aqIi@wkh>9sJ$M5x?mab-kgIrJES_k?>+m{F=4IO z?C07JXc9hbb2dw)_70DRzDkOV64bvF;yW>faLnadkF`99yjwHMQ`f>c)ju&saV`2} z8PMnQJ!o5uH$>f)_kEiBlw~W^oQqXBGht$XwyMolN$<3ZGZYZysu2dJ$KquMk3*MP z)xT;g)q}}nI8r}!ix8QeZ3K4jv{xQeA3t?*c`tQE10>|i`B0&YShKO_#ntjoiD$(> zNi!&E$m6buX=m!%n+e~yLMBCt9*9Mv$r7`;@U!B`AaXLu?1Aeq$+|^bMRKN@6@3Z( zio(ahu?Y_I0|?Xw;9-H{hcxi#KI_!YtKc5&*WW9rx!8&Hg!LJ}D*Bl=3sM0vq= z09&@AzOAb3X-F%%`9}2`ptyQs01j216fVlCd}*fBU+*ML2kCnjfPY|BFq4Z&^Q)`1 zSFrj(15rSTcXC5qA(4xz*7?AmHrwsL(zH^!#yw2frteTu#G zwZ1Vl%IOmT?W8Eh2MYd9y#6bO?=z_?*5umbkgVnaQgEG|)ZEHg^SyZyroExDzlotY z%AZrA`l?SySTsH7Yx}qleI_qFO!9_W02~WoiK|^j9!$%uyg1)b!@!r|U`N6+KEw%U z4eV}c!FvG5%{Do|qk!i(Aej;nuvqf+!8)1Ybq4wq<28Dnil$S*bMX1$NQ&$x_4;I77Y9_; z4c`MqFBk^Z(4Ajg{L$x~iO3-lc;ZLR>%Hn}HyRvk7k%;jB;(VFKzC{4BEI$-k_aGt zXHr3Yb-%%AAKy)LYVxaew%b1k~2ny8o)elB(9 zmd!|oWDCETVi*R*aBi57TBC!6Yy`h^b6+)Z*oAv`Ev13^y1jx<%Q@I3dg6ufoLMq6 zSGDal+6-Hz?;Y8Dw|vgh|J0&NAN)<`v6|0}#MJ!L3jj2dU?5k!w^-E~Q=P#gKEv%1 zEEoqM3IMv?YMY^{>jF|vO@(4LU+$P;M^1`jd>{mhEB&0BDv9CNk&CytjOEfd=HJ@0mD}Z+wOUi9 zl(6IiM*@{0VnADs^%dZ@7<0W00)1yW7IptR9&7yH{m%ogT}&d_;sBMh#t=?i@5+}P zwyMU$B$p=bQ#=}Z;;3!2>Q?Z0t>`nax%P*>hX>6;WXI!?08NqeQqd<`^%>yrz;340 zSfp9(&_$caJF8u6)8$5SwSNZ>S8pWw38>X{{+N$T#nJf>560iV3pg5!HT{{`@ZgDs zbo(#u^~&F}S7vf%@*CDV4Pc>sc}QPRJxA*BNsuQXl>eK~e44~!7cjt8z}?NAn>M~I zf}E1=-A+W#1}KxxcgDV%hiWB@l9s`6tSXECrR5g+K>8i*;mEeOae@yV0LbyMn; z$6|i~M+8u}e)E|_BxS2!FlZY&`&<`YcFjiC=i{fxisqU_zVX%nk9}|gdXphF(Emr+ z6(GsGQ9DUag4&Z;0GKv{BincWd#*@%ifjb)#U;8(SC_V^FL&dwHuoyvrZ1A+XtQ?= zELB}>1%mxV??F>Cz#{4iGamt<^pJQ-Z&cv1MjpkPSOfpHxp$U9+F4#7$8SJEgs)Ri zE_*r$ClvrEqV5p}dHzQS=bP>RoRBm9*^H5tkmFC_EGdvYfa;7iGq2ea$!!wZs|aIWAn+0E0-?$QCC7O z1q2H_tdLKs2LR?#PS1fG{-yJn)&S5D@V<2U2yoZ~2syE4arsPEU@MM!S9@3N?LT^x zS3sj>IjLFv;ww6(}|9LCayC_W|iDKVNy7Y=tOR zYM|PlMovav&HH--k^{&7FK2-kU%r8JshB+8_+%GP6%(xsiI`RXKlxBN!fFJvlmHU( z{$0X(aTubvU6aJ?>^$z6yOq2U;JB@Kg;Ual%I?>q61|iSns_vl9a** zxLI{=CP&%-d@l7pzk^JNb3!7+A&sx=pdOKLgUm(6wC8%52NS4cL`4gfZ$)w!6~G-` z%E3<(z)~3Q3mAdFs3{V9+)?k28Xfor`Ga~GN-fCXJ|<{hja&cwPG0oo%ItY?dWc+K zAEoI0_#dhE6TetJGd6@l)9za$CSG(3JV8l2km;<@NP2O!pxGYNcpcvC>00<3wlzUs-pLdQ=~9}G=0 z8s~U;@8XN%-g$H-Ev`XMRiavNTnf$?@>*7q{exA^)?=bX#YY6$5Xp970WGDF70;~r z{Ev02F~I$N15o2+2iM%fvs`I^FE=WeYCFge(G^rRK0qLd8491~Z2s}IpFKaEK5j@1 zR8x$X(clKox5Hy3i8Pi%6c#Lfnu`u>EO!&cj65TIyiC*6kLyza1;))=P>fe~LUiDr zx}ON!&v%8|^w&{97rac)szi5>BH}4M4scuVIRIZ2=f56cy*^%gN4GE7T2xC5}k0e(IJ^CTVOyOyj2{~H0H-t>QkLZc+b zK>0Y`q}71tMU}NhV3&Kz8dOew$un;hY#0TOnFA7>+#TePFMyrttevF^(dyl}zX3sh z6k_<0cS4bPNKHd9-H7~+nJFS0mQ^q1rV$)8=Z1f!=Ny`te$s{*|+ z-yr4nr3E%&|BSu66c;hYbpY>ZlR;%)sT`j)RkQ7mY?vSX`Ykjh%O=ae8IW}5+{8f% zwt$=lH0h*#dD^QIO%-_nKhNh4?gXhPjH1Z_B_aLOf5*HdKhCdg2BIN12HXGkJXz^= z*?;2R7gu%XtiCOs0^3Ne88>9HY;BFe27@~L8!AkThfU|b-+`H$T>E>w)6C0MAKki0 zWm25Z_a3RxmchNPlT^X*Y~j%b1%f&pv1Hp;42OXy_}+*gxelegLI`A^h!TPuFS3^;45ZTpA2s0fCQ#^p zR1vOL(P`d`+v%T{mHt`vKGSrJ6h!2zI>f0Q=>*06rWipac6P$Ti=`QMR3Ag1{C4Jx zNEJ=Y)>{kzkPg>@iz*77dD@l~Y`BvqLZ^3}%h5HzH`t(`kx+(=`UMYA*~O^6gnuaV z%I>hEwH`ZkX@z6|ytlV`)~N5Z7JKaU3l3)FGkhhkubbpdzhbXBzHqZ?WC+sJ*9D}2 zS$!LO{`e$lEbxG-9{qYw@_&{0ol#AF-?pe=LqMcTRg{h(q7*?udWX;g2t<1CH3R~R zNC%ZJ9i&U>y(vg9p@m*UiPX?TsPFLJ{oniE9ltm3hx@+04`+;w?31?6Jaf;vvi91d zd&Ymc1*puxn+jj$O>Z#~6G#pgg-Xe@x~@6Nmvp^|_APlT(-!Fus_|CR#`;rnsTmtl zA>Gv9+&8V#DuGFjVVxPfXw*Z+eG5PGt1Y?FX?(96)~gclN@Utckx{eMH&r{MRb)X0 z4BlEYl+6A9uSH&Xyv4^iRg?CQ+E8SdBPT16({}Byrxn~6B5ki|YAPs`1;kya;nNGF zXyBavxsbE9ls5Z@DE3pX>VkZIX`K`QEQx{VY8OAmS_Ux{$L=O)XNn+!1bP84gIi(@ z^?&>GPe0RfZQkwgkliI_mseHfo-Rj@l_i}l?S92I+3vJdWuMsCweQv43`MxbQp(T4 z{=(0tp-g15VT}DJUdsbds=;%-HnZ9)$@e`DD$g~W_w=AGGvfgqp9?0w;-nx+Ym5y; zJlvi?YIm7J8Q=4O8!d^h5!@Dk<+4_YNE?^mwW~2uq1M%7QHPkun(K-pP86@buF?>Y za|1t2UjsoMvM>s5f9fo(+_)rmJxtE#-2G79428C{+D2fc|9RkH(-66I`q2rFvu@MI`YZCO^RSACTANpH~< zp>4<}ddVY5owfMwK|AW5k=;caUtQktS?AK!8|9@Vo)K_U?LyOFgDQ)yPjIb?WnVWR zx~4C3;~V<>C*Ms1hbfwl9QuAHohbR_t#3MV6^*VHlh*MxzBaC1KedRfD*CGgOv}gr zI3>}*{6$H~gMxR%g%W}I7YkM|eJP%4r9)Ai+n*Vc{F$4T8WyU*JNQdp(Pi0>B-M1Y zpFWh{s-3BK5b5_ulcwjCuKtgArlOzAoNdey4_3_3WjVG9Mju%s*aSfcKMFJ9@?0+R z!uDxL%#M5ebZ)0kTot@CT zTD~inxmdr!fiqHvWWAoWeh%{?!V)^%SWcV}N6#<^Zg@Gu)BO_eIr-5ew=Z?e5nEJ@|LFks@JGkO0zizK)54roflaU3-L#fEi&%C>XYSLe zR;Hw{ymA{->XU47RIBiIVU!CY?WP)7k502V@mo6qA$7)>nH=u=S|mn*)Y4)TAAPc| zZFiFO_xd#kfB!qeq9e(SWn(>vIDyek_hG0)F*@xplakCN#@Dg&*GL5Ds}@as!aGAN zqU+261ArM`w@V6rB+>B9HN7-6u=SAtY^hHlzUTz#J$ZgfxNfmql-AiiuX7GcBTu*C6 z=GNo=)IxliX~@KPq7; zdk4p8$ffghxsB>~Y@}nXe|E&=vm|xtH$+-BTB1sqi~kDHtNp6go}pAMiN}&?v$t+Jj8MnfjUt}R)PAT3r;ql$pckn4Ir>LRTBLo8 z;i&Rx1{<|?%bq0d_C9T!q@1G5PE)WNtLt3ZJV;*S&P%6a#cvZ{x9s!y>Uo`@UI!bj z@~As+S(9FJ4QxNc3i8n?G*^$9Nm90{6e`a#2IW&xpY*6;C#Xc%N2Q8s8-fYU)>6=X z)7J<-N|Ve+*q4>n*{QgVC#K+#2p^%s&JdP{K=L5~u1rEtL@&=r&mW6GNik2q()*XH zFfd&F-gE8r7$6pDEZN@Y37<5Wos|TmqE^gU@B$l4v&u8`ttc*D2h&kLnoZ*Iqte|`cj&69i z{aKQBk)Cx_%bO-Z-Li`wPuJZI!5*B%x;9Z*?HBlV-dbSgPLlzpem&2{u zCXU~3iE!ePdAjq`%J4^~OxlZquh-&4XJk&L1am%I5} zA3W1=BI9HGM8-lN>S=sxlH8MbwnIYJlzZNhaabRoQDH87o_%2rW$ge*Wj=)&yF1s^ zod;|+XMWa2Lh#l(Am%paiyW5m1%;3TUx$s{^<&z{B8rFWF$OH^AWu6xcf_!hU%cd@H$^mi zH(CveH#&&xZt9n*wkMp@-Nyp*LDTT>S&h8qi4r|F%D3HkOM{~W&3y$;MODhzodXjM z)w(TbwSrQ;Ypur@Z={t~dDxNj-ARC;@u~U)Gv;#kCoDN18!ib(cILP{WxGIpU0{ct zTb~)nTEA@^+jReqp^`Z;@HeWBSFcx16k%f#|BiufpO3R}ZEhZlSd<&$E$U6OgpY{E zzn?SRo%9$z+F<^s^h11B>ZL7WjnOoZdVO;rF(M+JHvHMPw%V(JX`8YfTGKx+g%$M3 zZ0yUQ>1&m)!@z>b$@#v_{ubOWSEZ$7qUdI4FYuYCFF(&fsmq%@HVn30CyL1eJCxBX zEuT+%${qB?m;glnrXLkP7jX~urJE;uGtaNINiKVJimfPl6wD}*-MG7C8CQqv^-qYy zzP7cSU{42SWd9J}3zN>HJza!slHMZOpwA^O2=X z;;A9z8f&OUy4O9QqAv7l@QBQTee_^|L$QwkMZI?*bYwHJ-e-}lq#+Sd9{%!jjj;xs zjXxK(xug6yjr*Tu-z~AzDi%E28~r8(xijDNbimq#ySxt^wxX*#+SKs*!BAVObV^Zb zzsmugzOaq)u*n5ZP&`$d{Er=Vwy`YRroPGSjk8u?Zo!Dx>7hs|H8c3 z24j-$AZJ|%%!*2nqqdc(k%_C9OvhM^&H{u-|>1GclvS}g)! zTEHifn62iigRwm|oq+5g$9q|tld!)b3j9Zz*mxD@hy2TTwzik3wo?%kLv{cK(K9t$ z*%%sUzC6kvHVjO$`vQcW0gAo08+UJxk@E6fEz;cQIhHEzdc%$Y=Nh48*p5;4t!*tc~wdmkP#$x zb>c_ryH{(NuLdu;s?f&(tB_A6+wW!Y%|}C<6Ob6Hb3TWQH@9Sp(c(Bk$0Fpsg@sR)4Sx|OHR4`act8{_0^M$U~%y6|-&QSw*KMkfw&!#rNU-uS16KC#ZcrOfpy+;}olHz7pX*Lg~1;N-ii#{_c92)btq>Bco1k4S(qok{Ssy=~`Yr3k@ za9{hcZjKoSWiuC>Vg3Ug9tcYvpWzJNh;#`~+f`SyRke+PU}>7BhaU49(m|>H51H#6 zS{$5hT>uJzI>}tP5A1F*M**NEOD}vMF=CjSX0obehO&KP=F3vbN5-2=&W6aUI@1d<6~TL_62h$H`VyF zxMej1h%1obeeD}g|7Kf0Z>01^D7esYBMO8c)r?cCr4(>*9pW+?^)I7lpnr~frdf-V z!)z$BqSC4%JuTZ?gaiTfS*yH8=gyT`{&99JWFGf8U-cv)4BY}uB6wGomaG!^<<#e~ z1|pAjYZJMt61oQ@!Xp93ZdyAjM7B63{Cx3!B`&!i@Bs_TG18W)2Kmx24K=fBeTq<~ zW}>8%aAZOXh1C&*O`>qkrhU}j?^+Kn52H7ihh$?r&*8f_-B#6#P*)};?www?PZs)n zg7HX804>9eIMijr z$cdUY%2XjYq-4Ba-G_M*E6(eQ3~7#t{vi=CQ-hPYSU4IcP)5QsJ2~)E?O}X4rY|@- z`Fnl_(+zfi>-q>WzVUNLPdkWJ*TeU9lK~TfSqyuUw>dj)eYW|cod9S$CRb1=Wpz$! zhZFpMT;!&Bgr%xy>CFOh&I~?Mv>yk(3${eGIZIUzmL;tZDxH5o9#GucAWZw09pNTh zzplPrSNkwKMU0dGZRym+S1l^!(d|1Z=hSzFheNGpBc7PM@O#)IqwhBH=Jn2#Ky$&b zA){Q|q1%})H+Erj~e>`GHWB9)u3M`E-DlefW&r_S&ZDs1U|^9A-Qur~|q zr1E~KTwIj)i||_wJ4m^HvjsjkBpTmE($2#*Q{@2I1M$$o zFZA_Rak|o>Gran#%=q3Q zFJFw-HP;vSer7sHXLgx31&HuSOZ{SZx!@4A85GywHNWY?c%6ims<(p2NOQDT2s*cI zV8|AIk>S3#uv5nO$-iOV2Os91qd}v5FiOY7V#eN1nLVqq&W421 zu%&0Z@pfN|=6F^lvLvb3v!M_NpL+_~K4`Z$(#(Fv`RkW}TaNF-XC#eIzzArZg(99+rt&}{ggcv$Qm5# z5|jVCu(y>68esJJyu;h1S$I$-WqY9#Mq<;uXRsy!gpX{L%1L_Vak z;rN=@2R^<-MUDd8Kzq5VaNSLLDU}uSG7qgoEPgnSMX~G&_PKJT$~QgJM(1rWFgaUy zk6A3315(f80c+RrFzH+c)PEXAM87$g7ZkzqD+n)JZkjxI-s(g z%RiiBB;Q>ZWetXx70JHdgNl9K<3JU)!?5VA7u}~TA2c>ukdNeTp+>Y$FMDOQg^Y#a z9htRkL$7|<HzWL@4CwVYyx&5`44b${pMw}q zKk^#(vd^*|z&OmuLGEiQmrr^Q& z?{4#W`J)l>3FB;#n?nprb-gGm-Gu`IWcp7_WVI(IH{5!}NVooz;8*2O`3K$3;q=Wk z(eu`HZLgq-hPTSo^pqd#eokFSfjuq$;@;&SLyR}D>wc>Z1FJCv1Y`5MJ49W56# zQ}&fu`M#hzIJZrW@>{PU*#3`_|BCzG0DY{5H2PDATnt*;UT@3J?(qc47X#@P*502P zdUNnnbkcbEEI5d*pF8>WOW*MU=DYiZu?8+@W;k1>hdTHl>GQ5RfGdxBm~m?T z(|PidY7*pVAKHNa{tT;qxJ{=dL+0f<%it#1$!~4TZ>Fuu>n|h`FYM^~xf>c>?j`)~ zk8zs8e^TcFRNJ42a(KLjt`{v%52IBq@iA4osljnb1AT|hl>%0$<_FC%#gw5|UGq9M z9umJA^OLTS$LiKz%q7mG1*S4Ft8uq#OUJta(-sOS!bV03Gf0Y!u^4L)?@$;|Me#>e zD21ylYdc7D=86fcHcA+ogLsWkGu)?kc7TNE-rbxs=D63`gFWL4T?Xd&fPmI8tBv(c zJu7i}Y9uQIT(R-4BS4E2Xk!g6uaj5N>Gj7hx1 zit7rpQ>1S_mvFdR>GUfdq|thGAyWbF1w0PquQTV?Yt%2nt5=VT&3OLTm!;?g^Y=ao zLv4uVl?s>p5#5UHU9{BEXpKtG0ltw06Bz!DZI9t2!5)Jn5o#5xgSu>NbR&iz(Oz@9 zeVa7G`aoq(zC@ny5%O{3Bjlp)0ypxFw!d7S7fsp|E)L5ZnmLkeaRkS(VR;vBO(MUS z;|;{v^A5>0X=ll_w&Gv@X|VhYrc~<4kdPfjK(f)>JM>?;hETCb0b;=m!13;vE&qU|Fc5vghOhQ?F>BAuuMBDD9?@Ep5l8x%Hjb2_Slx6x4a3%V~1K9{xyLqaZ zb=S6eH(I4`(a34ia^)9a=NtFpiX7*hzRbH$2GlSxVaso@PYymLl_)8!f3tAwZfeWm zDEN~xBVLPUGA(_1OtrQ78acbQ~s9I<5A7@McsHI1GMO61u zLpIwj>~rdUdrPPtfsq!lnm@o4+2#K$DKN>}c(-=L0YA_`8O?tE+(&Qg zB#d!oc_cVpS>^!Q+`1r-r-AMD3hE##j@FYB^RP>yTo5Mj($k6+GWqbWZFKpOHh`r! zV@XTUdgr89D$C#KopqESdIjQxULGhm%Sw|54a`F{S!kH#8lo#kWgn0;ign|f6B>%C zF+A>+UbyPOz1nM13Goa_xAMOFoE@2iesTTg@8e81n@8-fRiy{tsl^OPM+hmiOSY~_ zI7bgaa%82WLXIp2F!Z@ID$-U81>sNA@^mAiEK@Ft`rNeSNj#guW-pL##orApL<#3m z3LwrWS(P5&ct$chkAFw+mG`aseb|qysgy{Nm7%1OZEDyVXz{s&K&fF$HJ`xD9x-Pj z;9Ls?^9EcJO%$yopv2C@UJMD5q8Sff1_i>Wr-?g7=$?x5r6qd_iSv%krQ z>Xh?c9k1u{^?ZJ?KKe0P#Gtl?llqy;(otNgZ{3u)(z z(=FD65vZI@j$F?KSKfuR_hD#2^BG}zv#GpekOc#IyA0v*^dZq{uao`C*m}#&%Ep+6 zChD4{+tUgCN|8UUZf&>SE7#$8d5i+q%_ug`I$mevCV`LhBP8Zu1!2d+lp;3+LeFAsybqTyQQR>$6=CY&})2S zmgx}ydd7-4a?q!0SH(6Nwc;!-8Y7gtdt9_P29`1U_e33|qt_sVm$g65?4d{_7V7e= z-(is7A75lzem9%m#e!+iHnW=e^b$OzW&7ig_xK59>l^b%AKBk-W)a50w@p)rejdw| z6l(5=v>L?(fm@>=?KFZ`uww3dYkabR&rQ%Pg_w*Z#f+O7$CeS73 zZ-2u=fevw{sEtL+A!E_mD@FnwKgb~=eB_jP_tq<*3V}uD9SJAPdVx>wj;Ek zSCQZP7BAa}$A~p-9}T=vjMtyey=J6V=;|$_lD^ z1!ch=2Ki$8{R^*}s*hX3JmV$(hyEe#S^Y2sr~6r6S&gk5s(4fZruG~IfrB#g?v;4K z0-WVAC2mJv)%*D~Ks!GBFzqR39k{*F8F>Tl?DK z*7C(-5&~Uh2bTz?6dFMUYn51|xA67??_PvT_$JM&I@ z*X5=%Srvw!%~b@ynQAedAZp0mETG#3mE8=5;Q^{|WI3^pt3=bM&<1bpW6JYAd2qu| z;@W=;!Ak1x)thI}WHUlqJTr)D(hsRb3mJQxCc?S7vJXX9ppdQ{HtL0F)$8{>FMLEC z#3ZtuUIj(%PoHhR8cw4?v_(H1QI6heYPdZxpDQ8+?4W@BCEgzk_7AVYs4ko_Z?1ST zN;A_i*&;((jQY$fbbD{9_b9W4E}MCYw)k$JhEm0qCEn(>E3pzmf9=?B&Tn=+zC7_R znK~>J0MOYDgNe_ZjtAYHx-SRp?c|=Bf@}GpY%|_{NjD(6XFyVU`cqJiA#Bh&B5$}+fbRu~7z3T-syoSnC_kHgKB6)XJcs=s(ZG9}!D2zc<{_dUgzH$D=C+w1L2`T*}opeU;*gOGav@qYlEI*%s+ diff --git a/images/tool_http_status.png b/images/tool_http_status.png deleted file mode 100644 index 19d3efb35cf04548e7e769eb78b0f3a4f53c5bea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33112 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

    H*Rs->*juSbyB|FBgIk)&zfqer>|Ifq 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 diff --git a/images/tool_jwt_decoder.png b/images/tool_jwt_decoder.png deleted file mode 100644 index 6349b081b1c42c965e0988bfd4da26134df05f41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36857 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 AX8-^I diff --git a/images/tool_regex.png b/images/tool_regex.png deleted file mode 100644 index 6dbc18eacec510386545949862e2e7a7433df760..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35148 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 diff --git a/images/tool_timestamp.png b/images/tool_timestamp.png deleted file mode 100644 index 4977a32a14e825fad5b4796237e1bdd38c355d39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21441 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 SU&MRI$W)ay6pJ6feD`k=0(UY1 From 733d00d4a52f98f7739039eba14321aa54399886 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:08:19 +0800 Subject: [PATCH 043/166] Move the fixed-pitch font helper out of terminal_view --- CLAUDE.md | 3 ++- architecture_explore.md | 4 +-- docs/updates/2026-09-b.md | 8 ++++++ docs/updates/README.md | 3 ++- .../connect_gui/ssh/ssh_command_widget.py | 5 ++-- pybreeze/pybreeze_ui/fixed_pitch.py | 25 +++++++++++++++++++ .../show_code_window/code_window.py | 5 ++-- pybreeze/pybreeze_ui/terminal_view.py | 21 +--------------- test/test_utils/test_terminal_font.py | 9 ++++--- 9 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 pybreeze/pybreeze_ui/fixed_pitch.py diff --git a/CLAUDE.md b/CLAUDE.md index 528b2fae..416a9f71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,8 @@ pybreeze/ │ ├── gui_thread_gc.py # Garbage collected on a GUI-thread timer, never on a worker │ ├── plain_text.py # as_text: server/file text shown in message boxes as text, not markup │ ├── exact_text.py # exact_text: a text box read as typed (toPlainText changes U+00A0, U+2028) -│ ├── terminal_view.py # Terminal output in a text view: fixed-pitch font, a lone \r rewinding the line +│ ├── terminal_view.py # Terminal output in a text view: colours, the pty size, a lone \r rewinding the line +│ ├── fixed_pitch.py # use_fixed_pitch_font: the system's fixed-pitch font, whatever the theme names │ ├── error_text.py # error_text: a tool's English error (exception_tags) in the IDE language │ ├── code_result_logs.py # Only warnings and errors from loggers reach the editor's Code Result panel │ ├── closing.py # may_close / AskingDock: tabs and docks with unsaved work are asked first diff --git a/architecture_explore.md b/architecture_explore.md index 56c7b727..7d0a9798 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -161,7 +161,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `ReaderGrace` / `any_alive()` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 - 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …`、`Task exit with code …`、行程仍握著輸出的註明,共 15 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 - 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 -- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,與 SSH terminal 共用);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 +- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);輸出用系統的等寬字型(`fixed_pitch.use_fixed_pitch_font()`,與 SSH terminal 共用);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 --- @@ -301,7 +301,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成 `TerminalOutput`:是否先清畫面(`clear`、`reset`:`split_at_screen_clear()`,只留最後一次清除之後的文字,清除前設的顏色延續、`ESC c` 則重設;widget 先 `clear()` 畫面並忘掉待套用的 `\r`),以及一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`terminal_view.use_terminal_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成 `TerminalOutput`:是否先清畫面(`clear`、`reset`:`split_at_screen_clear()`,只留最後一次清除之後的文字,清除前設的顏色延續、`ESC c` 則重設;widget 先 `clear()` 畫面並忘掉待套用的 `\r`),以及一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`fixed_pitch.use_fixed_pitch_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 7868b906..25a14f1c 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1006,3 +1006,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Done**: deleted. The README screenshots left are exactly the ones the three READMEs show (20 files). - **Files**: the nine images - **Open items**: none. + +## U-20260924-312 · 2026-09-24 · Move the fixed-pitch font helper out of terminal_view · #refactor #ui + +- **What** (refactor, no change in behaviour): `terminal_view.use_terminal_font(view)` is now `fixed_pitch.use_fixed_pitch_font(view)` in the new `pybreeze_ui/fixed_pitch.py`, typed for any `QWidget`. The run window and the SSH terminal call it as before; the tool tabs that show code are to use it next, and a terminal module is not where they should find it. +- **Tests**: `test_terminal_font.py` calls the new name; the 248 tests matching terminal, ssh, code window and run window pass. `ruff check` clean. +- **Docs**: `CLAUDE.md` tree, `architecture_explore.md` (run window, SSH terminal). +- **Files**: `pybreeze/pybreeze_ui/fixed_pitch.py`, `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `pybreeze/pybreeze_ui/show_code_window/code_window.py`, `test/test_utils/test_terminal_font.py`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f41796a5..70d8a4bf 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-312 | 2026-09-24 | Move the fixed-pitch font helper out of terminal_view | #refactor #ui | [2026-09-b](2026-09-b.md) | | U-20260924-311 | 2026-09-24 | Remove the images nothing shows | #docs #cleanup | [2026-09-b](2026-09-b.md) | | U-20260924-310 | 2026-09-24 | Redo the README's main window and correct its caption | #docs #readme | [2026-09-b](2026-09-b.md) | | U-20260924-309 | 2026-09-24 | Keep library debug records out of Code Result | #fix #editor #logging | [2026-09-b](2026-09-b.md) | @@ -393,4 +394,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 109 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 110 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index c6e93ccd..8bd6a265 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -23,7 +23,8 @@ ) from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import load_private_key, unloadable_key_reason from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_login_widget import LoginWidget -from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, style_format, terminal_size, use_terminal_font +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, style_format, terminal_size from pybreeze.pybreeze_ui.thread_keeper import if_alive, let_run_out from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.utils.logging.logger import pybreeze_logger @@ -284,7 +285,7 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: def _setup_ui(self): self.terminal.setReadOnly(True) self.terminal.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) - use_terminal_font(self.terminal) + use_fixed_pitch_font(self.terminal) self.command_input_edit.setPlaceholderText( self.word_dict.get("ssh_command_widget_input_placeholder_command_line") ) diff --git a/pybreeze/pybreeze_ui/fixed_pitch.py b/pybreeze/pybreeze_ui/fixed_pitch.py new file mode 100644 index 00000000..374257ca --- /dev/null +++ b/pybreeze/pybreeze_ui/fixed_pitch.py @@ -0,0 +1,25 @@ +"""A text view in the system's fixed-pitch font, whatever the theme names.""" +from __future__ import annotations + +from PySide6.QtGui import QFontDatabase +from PySide6.QtWidgets import QWidget + + +def use_fixed_pitch_font(view: QWidget) -> None: + """Show *view* in the system's fixed-pitch font, at the size it had. + + Output laid out in columns (``ls -l``, ``df``, a table a script prints) + lines up only when every character is as wide as the next; in the + interface's proportional font it came out ragged. + + The family is also set in the view's own style sheet, which this replaces: + a theme's sheet (qt_material names a font for every widget) overrides + ``setFont``, and the view's sheet overrides the application's. The theme's + size stays. + """ + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + size = view.font().pointSizeF() + if size > 0: # -1 when a style sheet gave the size in pixels + font.setPointSizeF(size) + view.setFont(font) + view.setStyleSheet(f'font-family: "{font.family()}";') diff --git a/pybreeze/pybreeze_ui/show_code_window/code_window.py b/pybreeze/pybreeze_ui/show_code_window/code_window.py index 721af055..6631c2ac 100644 --- a/pybreeze/pybreeze_ui/show_code_window/code_window.py +++ b/pybreeze/pybreeze_ui/show_code_window/code_window.py @@ -8,7 +8,8 @@ from PySide6.QtGui import QGuiApplication, QTextCharFormat, QTextCursor from PySide6.QtWidgets import QWidget, QGridLayout, QHBoxLayout, QPlainTextEdit, QPushButton, QScrollArea -from pybreeze.pybreeze_ui.terminal_view import insert_rewinding, use_terminal_font +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font +from pybreeze.pybreeze_ui.terminal_view import insert_rewinding from pybreeze.utils.terminal_text import strip_terminal_controls, take_leading_backspaces if TYPE_CHECKING: @@ -58,7 +59,7 @@ def __init__(self): self.code_result = QPlainTextEdit() self.code_result.setLineWrapMode(self.code_result.LineWrapMode.NoWrap) self.code_result.setReadOnly(True) - use_terminal_font(self.code_result) + use_fixed_pitch_font(self.code_result) self.code_result.document().setMaximumBlockCount(MAX_OUTPUT_BLOCKS) self.code_result_scroll_area = QScrollArea() self.code_result_scroll_area.setWidgetResizable(True) diff --git a/pybreeze/pybreeze_ui/terminal_view.py b/pybreeze/pybreeze_ui/terminal_view.py index d3668349..fb379904 100644 --- a/pybreeze/pybreeze_ui/terminal_view.py +++ b/pybreeze/pybreeze_ui/terminal_view.py @@ -4,31 +4,12 @@ """ from __future__ import annotations -from PySide6.QtGui import QColor, QFont, QFontDatabase, QFontMetricsF, QPalette, QTextCharFormat, QTextCursor +from PySide6.QtGui import QColor, QFont, QFontMetricsF, QPalette, QTextCharFormat, QTextCursor from PySide6.QtWidgets import QPlainTextEdit from pybreeze.utils.terminal_style import PLAIN, Colour, TextStyle, colour_rgb -def use_terminal_font(view: QPlainTextEdit) -> None: - """Show *view* in the system's fixed-pitch font, at the size it had. - - Output laid out in columns (``ls -l``, ``df``, a table a script prints) - lines up only when every character is as wide as the next; in the - interface's proportional font it came out ragged. - - The family is also set in the view's own style sheet: a theme's sheet - (qt_material names a font for every widget) overrides ``setFont``, and - the view's sheet overrides the application's. The theme's size stays. - """ - font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) - size = view.font().pointSizeF() - if size > 0: # -1 when a style sheet gave the size in pixels - font.setPointSizeF(size) - view.setFont(font) - view.setStyleSheet(f'font-family: "{font.family()}";') - - # Smallest size given to a program for a view squeezed to almost nothing MIN_COLUMNS = 20 MIN_ROWS = 5 diff --git a/test/test_utils/test_terminal_font.py b/test/test_utils/test_terminal_font.py index 8bf2e614..1e2e234c 100644 --- a/test/test_utils/test_terminal_font.py +++ b/test/test_utils/test_terminal_font.py @@ -12,7 +12,8 @@ from pybreeze.extend_multi_language.update_language_dict import update_language_dict from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow -from pybreeze.pybreeze_ui.terminal_view import MIN_COLUMNS, MIN_ROWS, terminal_size, use_terminal_font +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font +from pybreeze.pybreeze_ui.terminal_view import MIN_COLUMNS, MIN_ROWS, terminal_size @pytest.fixture(scope="module") @@ -32,7 +33,7 @@ def test_the_view_gets_the_fixed_pitch_font_at_its_own_size(app): font.setPointSizeF(13.5) view.setFont(font) - use_terminal_font(view) + use_fixed_pitch_font(view) assert view.font().family() == _fixed_family() assert view.font().pointSizeF() == 13.5 @@ -44,7 +45,7 @@ def test_a_theme_style_sheet_does_not_take_the_font_back(app): theme = QWidget() theme.setStyleSheet('* { font-family: "Arial"; }') view = QPlainTextEdit(theme) - use_terminal_font(view) + use_fixed_pitch_font(view) view.ensurePolished() @@ -70,7 +71,7 @@ def test_the_run_window_shows_a_fixed_pitch_font(app): def _view(width: int, height: int) -> QPlainTextEdit: view = QPlainTextEdit() - use_terminal_font(view) + use_fixed_pitch_font(view) view.resize(width, height) view.show() QApplication.processEvents() From 1956c333014b597191d9605fdd11e7f5e3713d6d Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:16:44 +0800 Subject: [PATCH 044/166] Prefer Consolas to Courier New for terminal output --- architecture_explore.md | 4 ++-- docs/updates/2026-09-b.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/run_output_window.png | Bin 45343 -> 48411 bytes pybreeze/pybreeze_ui/fixed_pitch.py | 23 +++++++++++++++++++---- test/test_utils/test_terminal_font.py | 18 ++++++++++++++++-- 6 files changed, 49 insertions(+), 9 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 7d0a9798..2f90c994 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -161,7 +161,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `ReaderGrace` / `any_alive()` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 - 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …`、`Task exit with code …`、行程仍握著輸出的註明,共 15 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 - 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 -- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);輸出用系統的等寬字型(`fixed_pitch.use_fixed_pitch_font()`,與 SSH terminal 共用);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 +- `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);輸出用等寬字型(`fixed_pitch.use_fixed_pitch_font()`:有 Consolas 用 Consolas,否則系統的等寬字型,Windows 上是 Courier New,與 SSH terminal 共用);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 --- @@ -301,7 +301,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | -| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成 `TerminalOutput`:是否先清畫面(`clear`、`reset`:`split_at_screen_clear()`,只留最後一次清除之後的文字,清除前設的顏色延續、`ESC c` 則重設;widget 先 `clear()` 畫面並忘掉待套用的 `\r`),以及一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用系統的等寬字型(`fixed_pitch.use_fixed_pitch_font()`,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | +| `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成 `TerminalOutput`:是否先清畫面(`clear`、`reset`:`split_at_screen_clear()`,只留最後一次清除之後的文字,清除前設的顏色延續、`ESC c` 則重設;widget 先 `clear()` 畫面並忘掉待套用的 `\r`),以及一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用等寬字型(`fixed_pitch.use_fixed_pitch_font()`,同執行視窗,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 25a14f1c..24616adb 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1014,3 +1014,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `CLAUDE.md` tree, `architecture_explore.md` (run window, SSH terminal). - **Files**: `pybreeze/pybreeze_ui/fixed_pitch.py`, `pybreeze/pybreeze_ui/terminal_view.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `pybreeze/pybreeze_ui/show_code_window/code_window.py`, `test/test_utils/test_terminal_font.py`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-313 · 2026-09-24 · Consolas before Courier New for terminal output · #fix #ui + +- **What**: the run window and the SSH terminal took the system's fixed-pitch font, which Qt gives as Courier New on Windows: thin strokes on the dark theme, and at the size the tool tabs use it lost the underscore (`json_body` read as `json body` in a render of the HAR tab's code). +- **Fix**: `fixed_pitch.fixed_pitch_font()` takes the first installed family of `PREFERRED_FAMILIES` (`Consolas`, what VS Code shows code in on Windows by default: `Consolas, 'Courier New', monospace`) and falls back to the system's fixed-pitch font (Menlo on macOS, DejaVu Sans Mono and the like on Linux); `use_fixed_pitch_font()` uses it. +- **Tests**: `test_terminal_font.py` gains `TestTheFamilyChosen` (Consolas when installed, the system font otherwise; the font database is replaced, since the offscreen platform lists no families) and compares the views with `fixed_pitch_font()`. +- **Result / numbers**: `test_terminal_font.py` passes (8). `ruff check` clean. +- **Docs**: `images/run_output_window.png` redone the same way as in U-20260924-297 (a real run of the same demo script, 900 × 620), now in Consolas; `ssh_client.png` shows no terminal text and is unchanged; `architecture_explore.md` (run window, SSH terminal). +- **Files**: `pybreeze/pybreeze_ui/fixed_pitch.py`, `test/test_utils/test_terminal_font.py`, `images/run_output_window.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 70d8a4bf..bd4e87b1 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-313 | 2026-09-24 | Consolas before Courier New for terminal output | #fix #ui | [2026-09-b](2026-09-b.md) | | U-20260924-312 | 2026-09-24 | Move the fixed-pitch font helper out of terminal_view | #refactor #ui | [2026-09-b](2026-09-b.md) | | U-20260924-311 | 2026-09-24 | Remove the images nothing shows | #docs #cleanup | [2026-09-b](2026-09-b.md) | | U-20260924-310 | 2026-09-24 | Redo the README's main window and correct its caption | #docs #readme | [2026-09-b](2026-09-b.md) | @@ -394,4 +395,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 110 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 111 | diff --git a/images/run_output_window.png b/images/run_output_window.png index a3123934e9e2d3f54467a367a612bcc31f45ba7c..4b54f12d35575b9a38ca2cae86cf60f3e1c5643d 100644 GIT binary patch literal 48411 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 literal 45343 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 CzQbAo diff --git a/pybreeze/pybreeze_ui/fixed_pitch.py b/pybreeze/pybreeze_ui/fixed_pitch.py index 374257ca..f57bf479 100644 --- a/pybreeze/pybreeze_ui/fixed_pitch.py +++ b/pybreeze/pybreeze_ui/fixed_pitch.py @@ -1,12 +1,27 @@ -"""A text view in the system's fixed-pitch font, whatever the theme names.""" +"""A text view in a fixed-pitch font, whatever the theme names.""" from __future__ import annotations -from PySide6.QtGui import QFontDatabase +from PySide6.QtGui import QFont, QFontDatabase from PySide6.QtWidgets import QWidget +# Tried before the system's own fixed-pitch font, which on Windows is Courier +# New: thin on a dark theme, and at the tools' size it lost the underscores. +# Consolas is what VS Code shows code in on Windows. +PREFERRED_FAMILIES = ("Consolas",) + + +def fixed_pitch_font() -> QFont: + """The first installed of ``PREFERRED_FAMILIES``, else the system's fixed-pitch font.""" + for family in PREFERRED_FAMILIES: + if QFontDatabase.hasFamily(family): + font = QFont(family) + font.setFixedPitch(True) + return font + return QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + def use_fixed_pitch_font(view: QWidget) -> None: - """Show *view* in the system's fixed-pitch font, at the size it had. + """Show *view* in :func:`fixed_pitch_font`, at the size it had. Output laid out in columns (``ls -l``, ``df``, a table a script prints) lines up only when every character is as wide as the next; in the @@ -17,7 +32,7 @@ def use_fixed_pitch_font(view: QWidget) -> None: ``setFont``, and the view's sheet overrides the application's. The theme's size stays. """ - font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + font = fixed_pitch_font() size = view.font().pointSizeF() if size > 0: # -1 when a style sheet gave the size in pixels font.setPointSizeF(size) diff --git a/test/test_utils/test_terminal_font.py b/test/test_utils/test_terminal_font.py index 1e2e234c..44d71b16 100644 --- a/test/test_utils/test_terminal_font.py +++ b/test/test_utils/test_terminal_font.py @@ -12,7 +12,8 @@ from pybreeze.extend_multi_language.update_language_dict import update_language_dict from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow -from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font +from pybreeze.pybreeze_ui import fixed_pitch +from pybreeze.pybreeze_ui.fixed_pitch import fixed_pitch_font, use_fixed_pitch_font from pybreeze.pybreeze_ui.terminal_view import MIN_COLUMNS, MIN_ROWS, terminal_size @@ -24,7 +25,7 @@ def app(): def _fixed_family() -> str: - return QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont).family() + return fixed_pitch_font().family() def test_the_view_gets_the_fixed_pitch_font_at_its_own_size(app): @@ -95,3 +96,16 @@ def test_a_squeezed_view_still_gives_a_usable_size(app): assert terminal_size(view) == (MIN_COLUMNS, MIN_ROWS) finally: view.close() + + +class TestTheFamilyChosen: + def test_consolas_comes_before_the_system_font_when_it_is_installed(self, app, monkeypatch): + # The system's fixed-pitch font on Windows is Courier New + monkeypatch.setattr(fixed_pitch.QFontDatabase, "hasFamily", staticmethod(lambda family: family == "Consolas")) + + assert fixed_pitch_font().family() == "Consolas" + + def test_without_it_the_system_font_is_used(self, app, monkeypatch): + monkeypatch.setattr(fixed_pitch.QFontDatabase, "hasFamily", staticmethod(lambda family: False)) + + assert fixed_pitch_font().family() == QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont).family() From e69faecdb038c03f4283fa73f07acd5d6a3980d1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:19:58 +0800 Subject: [PATCH 045/166] Show code in the tool tabs in the fixed-pitch font --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 10 +++ docs/updates/README.md | 3 +- images/ai_code_review.png | Bin 31930 -> 31048 bytes images/tool_curl_import.png | Bin 52029 -> 49598 bytes images/tool_curl_import_action.png | Bin 54704 -> 52909 bytes images/tool_diff.png | Bin 32622 -> 34382 bytes images/tool_har_import.png | Bin 47628 -> 50806 bytes images/tools_montage_a.png | Bin 123283 -> 123059 bytes .../connect_gui/url/ai_code_review_gui.py | 2 + .../pybreeze_ui/tools_gui/curl_import_gui.py | 3 + pybreeze/pybreeze_ui/tools_gui/diff_gui.py | 4 + .../pybreeze_ui/tools_gui/har_import_gui.py | 2 + .../pybreeze_ui/tools_gui/json_format_gui.py | 3 + .../test_utils/test_code_boxes_fixed_pitch.py | 70 ++++++++++++++++++ 15 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_code_boxes_fixed_pitch.py diff --git a/architecture_explore.md b/architecture_explore.md index 2f90c994..c61f12cd 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 132 個 `test_*.py`、2405 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 133 個 `test_*.py`、2416 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 24616adb..5b089863 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1024,3 +1024,13 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: `images/run_output_window.png` redone the same way as in U-20260924-297 (a real run of the same demo script, 900 × 620), now in Consolas; `ssh_client.png` shows no terminal text and is unchanged; `architecture_explore.md` (run window, SSH terminal). - **Files**: `pybreeze/pybreeze_ui/fixed_pitch.py`, `test/test_utils/test_terminal_font.py`, `images/run_output_window.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-314 · 2026-09-24 · Code in the tool tabs in the fixed-pitch font · #feature #tools #readme + +- **What**: the text boxes that hold code showed it in the interface's proportional font, so indentation and the columns of a unified diff did not line up: Text Diff's two inputs and its output, the cURL import's command and generated code, the HAR import's generated code, JSON Format's input and result, and the code to send in AI Code Review. +- **Change**: each of those nine boxes calls `fixed_pitch.use_fixed_pitch_font()` as it is built (Consolas where installed, U-20260924-313). Boxes that hold prose or tables (HTTP status, header analysis, response analysis, hashes, timestamps) keep the interface font. +- **Tests**: new `test_code_boxes_fixed_pitch.py` (9, one per box: the font and the view's own style sheet name the fixed-pitch family); all failed before the change. The cURL, diff, HAR and JSON format widget tests pass (92 with the new file). +- **Result / numbers**: 2416 tests in 133 files (the count after U-20260924-313's two font tests, which that entry did not add to the map). `ruff check` clean. +- **Docs**: redone under dark_amber at their sizes with the same content: `tool_curl_import.png`, `tool_curl_import_action.png`, `tool_har_import.png` (a HAR of the same four requests), `tool_diff.png`, `ai_code_review.png`, and the JSON Format panel of `tools_montage_a.png`; `architecture_explore.md` (test count). +- **Files**: `pybreeze/pybreeze_ui/tools_gui/diff_gui.py`, `pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py`, `pybreeze/pybreeze_ui/tools_gui/har_import_gui.py`, `pybreeze/pybreeze_ui/tools_gui/json_format_gui.py`, `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, the six images, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index bd4e87b1..0d4a303d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-314 | 2026-09-24 | Code in the tool tabs in the fixed-pitch font | #feature #tools #readme | [2026-09-b](2026-09-b.md) | | U-20260924-313 | 2026-09-24 | Consolas before Courier New for terminal output | #fix #ui | [2026-09-b](2026-09-b.md) | | U-20260924-312 | 2026-09-24 | Move the fixed-pitch font helper out of terminal_view | #refactor #ui | [2026-09-b](2026-09-b.md) | | U-20260924-311 | 2026-09-24 | Remove the images nothing shows | #docs #cleanup | [2026-09-b](2026-09-b.md) | @@ -395,4 +396,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 111 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 112 | diff --git a/images/ai_code_review.png b/images/ai_code_review.png index 984a7444d25c9987ac527c5301288c4c56e3a910..38185e7724d0411b2ff61c2478ce76f78462d85b 100644 GIT binary patch literal 31048 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 CI3*GQ literal 31930 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%0D((nkCJWWU|B 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 F{a>krrjP&t diff --git a/images/tool_curl_import.png b/images/tool_curl_import.png index 7aa8d540a374e30986167fdb79af662df25953f9..393fd36adb67fb335ff192a59cac531b74185201 100644 GIT binary patch literal 49598 zcmeFZbyQsOx+Pe+yAzz?5FkKs3l1R=+%>op+#P}@Bsf7raJNDV*C0WHQ^A7;cZWVj z?z{b7-#gROJ%7!tt_ABrtz$p?zWwdJ&v~!!*A_LsU(=jOpdVhZC zlDO6j-5Ozie8t&=O>e&sqC8e3X@a%GPNQk>!WcR9^d+SqKLK*C4Hy!C|Mo;dcuDv8 z5$_H95C!{uSK+34`uNqcIwtD-$M0?l|JQdLLr^OwM0uDEoz=rx*!%j6n=QrvJr(Eu zjw4Cv<4GOq(+Z#5xA?QYl#+r2S=N%xR2I_O3~8|&w&81)PHn;ur*jkat7P^hTD%wA z{Kw5No(Rw&2AR^!t~QAJ(teazt=#WU<#xxf3)d?%I$79brN(>wEHH{;k=5(43wYcj z2F#!Fkn`E1f{N6IiA(WQ%pRP?&F|7H$NwB9pYK7%uMl4ZTwtB=-=htYa7urfuLcDf zdor#KFwL)uJ7IOyaFW1-N;HaNWllCt4)(JT_h#6}F0*f)7(7}MsVdZ;uqCs1wHbSw zb*F1u&w#N&Ikrix0+;<%%!KGSVBvsUDh_>KsaI~i*!R^a-4WP*oZ@#;kvl!jA|w^Z zC_#UwN>*b=9o*-tJouVLmdHSdCi#l6!%t%l!m0V4I-DTjmGb+$vD~{)mq#~PvnNB? zuhInI)QTrd9ePKGeh8g;9U0HI90}jHtf90_wP&}MolzG@I<@J=FL^k0$Eq+&=A*-d ziZobvFIRj;3<|+R@BR)y(I*wSTxmK^`aFT8M|L+QxZm+Lc2Rm(BGTl~&kZ=aKG zjNTJQCQ$V9e1Vv)mpF0RU27DDgiyqD0{sP#fWUQ7n>Kp~r4m^v#t_T65M=Zh!A_#` z>d5nn2oE^|GJjKAa_cxFhPp7bT&=+HuLa;=|hd zjF0&w${JTypz&?#4u)5`r*X@*j|i3BJn5*L^}!T4D>J1TXQs2=IHBJ+Ppl(||0c(E z9VJ(l`EV{)<#1?!X{>2SX|{%s3`eU35yZ^S-qTY&;k>fSkT62_IEyqXjF7_KiG7I= z4czU_Zhr_FwG0TM6b|guE^P}RfBW))f2r$2KS1Km8@v}zcX*whw$gHPoo}B5Ke?~> zbc&`}+4WiwyJcft(*sZX)9G}|`$&|Mb5%=wal{>^rsJMi&ws1#_Au*bE4Db>a1bOFbm-^6qGdj`9vOS z0gE(YvNUE}T}-yVM02m3iy;Kjrq0Pm^jL z=^drVn07%@3I9^tcDg9o#a!0(GU|55VP5LFo+D*wsz9FK@-TUG0^=b9gqhJ?acHF1Y6 zG0@R5aS9fydigknbR}M-xcH9&;fe)0>~H1_h=!wm{OV)2nS}h~cfSk&Z+A=dZZ^%3 zg>MEYeSPCVqe%a2<$)iDn-X2}29h*;?lt6k`cl z6u6`8ikvTRCnKyi-X|wpaXUfClfGU0>pp$^I+`PSqdXmF(0O@>S!7XSC1A~0Rd;@9 zK`m?Mz+;0Y2?RU(=dZTP^eSAm_=>o`YqAxuHsuZn;rR9&YXf$>nFO#zzV&W`!sZP%~SqlxY((wp|g%Q0xpZc&)Sf}wLlV! z;(($4JzttISx>yAeo*Y#c+!PJ{F%d?)~ zob8oAHP1J_IR{2+mya$n{o25|<|v4H{S#Rz;cbf1btYjeK_qTE8CsAUM1l_ zY2{D0U^my%8_h@1=`s2GCDapSmf|&(@cM)|c$Y8|*%4TL3_=S^HNCoT=__ zI}T7#OW-~G0PC@zYHK1PK8ck&`<}-e6oo^-3{ctqn@!Dh&Jkn2+_@dH*}zHFXICEu zoGOj8pGl@3%7~@6ehfCZQoi(Zc1}BKnIZgBHhY#rg)u|4dNz_Jo_gcsv>4}m{o0#L zFFMaE_WN)(4bx}{D0~wyt8xj+?B>d~Vxj)3Q{6t;!M=g_^p*xntGv4e(gq*!=V)oN zP?hZ7_7l_5ef6C)e3((9B{>|iV|DYU1L4Rjn)2!%$tSphz^3Vx$QHDG3)2=Zeulh4 z?O(!UW`=ddBtxYNW4&(QAdycduT3HmAS(|@TE=faLf}V=6CD0nuY(>HfZ-&DPgze z=VrI={HZCe*HN*A4!f4Yk1CT9{_xq$S`GnRw$0UQ@;WEWC#-zVYS`$)m2LdpiNj6wp?&F?+PeERqd^iu z(AFJ^zGg%Db~9$1*T8~(Wt5yCDLRR&0WvO|F&n>r3F=q^M((H>#OtGj-DJ?QNS}q| zS(?l^I+>S5xprm5$1m;fJ>*-vJ1I$?c)C3oV(D6&tCOLO! zk2~hc^vJ;xpoi=d z?9BtSV^eQ8b74?7!&pEzG|w)t+Cz^teTFkTx7`kGH(s2CPg{a65|9-Yb^FusfYxv2 zy+mwkQVdT@OJLwm##>74>=*D7#irs7-9=4Jjp?R+QMmlro*|L%L(<0ETSB zgb*0L<1mYL(n}S+Y4}`%palQJM{^>MLQbj@c9{ioS9Z$H%l+4Pde1WjoVph2k=d8Z zdi&B=1YG-~dY{xe4xxbtd}BVi&LQRvL?bQP92sQduh90kdwc1lc&-z!l^BXIN(3v- zu+LTv5&P{4n{~wB$yhJw1h>GKLWKk%0Y0HbJApq}!oiA}Ehas+O1RTKwL5R!_#`sPXjR+}6@6DZk@NV=Ey~{yE$a(UtAlra$#({9>v@hvb{IObf z+~l|!nhGbzV~n8}%}*P~q8)Bdz3R<#x;=@3<=MJlffA+|4$2$ol}e_WWg`C|j~F?( zzmNRx{p_eMN4B>{S@LQ#3Nr)qhJ_~n14*52J+1ABJD@mv93Q3*h z-?za%x_UWk9lK$;v6t*P&~suq&})a-qoT~V4+_C_tweD~LqT)9xASsg@-8bSXe+t+ z@(f^8gZ)7pW;`faZ2rj^7(46p1d;Zh-k(*n*-Mqo)e7v2ioKGzl*tJ=-NTjWhkj2O zHUf4Ul`A(xVNs?sFg5N;Q~a4nav{XE`71Udh0o{D42$AyLsxE|;$?Fc-RXOa2b-}> z7okdYOS%<5h-oQ*q^U&=o!%+$StXu7ct4;wbZP^Mr)ml!c6)s2#_2-9D3}Om7O(Dzsxvf zMn!EoHtk5sUQqlr_fp|25u;%vI<)vzm7|G(^imVv5*r8npccqEI9Q)!sGi@AP{m}! zji-jlj3lze2y!n~sy?tj`ZSS*$%Egp#pSLGw@S`JL52MUSSQZzyh6Q#*dI&urQ#Lw z)NcK9PCr3PdEIkBO|Lq_OBtmj6u1kFw-w}i23ah{E?{Yp&^J=_Y9LUm(6bheANc*~G+kI^mbG zc`Cn^-?uU$YtX1Ab|#!*+tsAOlV#%_I3fWQnHPnSMJa)m7{A!A~=v6wF`ory5E!4t@m@K7+e&Zuc}2?0Kw|TNSq*KYfydEI?lh}jbxlX2-~8Qa@b$W4 zkv8@KKPpMTzDh^25TZEry?XhCFI!^}78lYBJw#wjw} zVtQr_w-d7Y#j1P}yn~IhZ$Tb_rawi7Eyv+Fb-YDp^!pg;fI;WN-W#RdPT0gzNNW6N zxg?Xn@SCTWc!xWrE!n11c;X%RbE7yGsSuZybTqWu6CmP+X*o`d5!E?@IE?dDTPSlC9MGfT98 zDc7%%wF>Ni6kcDC*MEF?bN=rt&lOSIy*e&nvADJWqjpNzhv>=*H)xryF5`+^co6@r zqHe}}12;gp zr@rj5^4vOaJb>?_4xCvF2P40triyMOsNpUvL~o{JOAf3&vwpx1eeS|B5u$~)o+_>w z8Z+}lzRAApIDt3yMi~xSWk9m_8huy+RmQfNj3woVj?nASkMCTMPT&wuBUS}{lIWOA!rmBq3j85!ZtM+cNNEi4?fmO2di|DAg<8STm=VFXa_N9uM8q0R*XaGtJ zz%^XiA3s8SSFgID$azGN2fL2&dns{-Iv zmTj)Iy{6##y@SM_?Q;s@oz(vKCyW}t649sj7zLHg9ohulhuaU~U$5woT}%7Z_w?Ls z^QEH{nHxm@-o9f=6`X0I00U6{?ml4oD6D@lf|Outv5mE~QG}2jO^(QXyxb{ezAcPI zml7Z747al+{nGO(#3cNc<(gWmXu;8AY)YJLjfc)1q2ehGjb#L}l*zx3W)QR8$ZD#uF}ynL2( zFZEm=aape*zf2Pjl&Q?OVo*XhTBp&t^q1}Uj&WfqA+Ur9~dY<>tFSs?sfqSDZZ8oOFikbO2bZAD|tHk?gZ0eyRx(nijxVE6%)aciG zYl&=F-Gu88adjno>?2m=m!F8s7viNvQmr$Xd#7-fn_K_6(go7Z+}aWTvO?tC)j6V|Aq2%la?Q4MpI+;u@bu-dDv zug&ed`j4dipLSx*-NBW4hhCw=6O4|VN46S?s}~j@KPXU&<6wL=2{a=VYItV&aJ(@j z3I>4n{EMbL3wod#$NWGKU242z2*@N^Ih^~-O@4Z?KPMD`6b{xDI7X+`Ui&bF*mBcM zeuJO4+u6;-F?1g!6!;n0fjs3Pw7tRm{DA6hrTV}_K^vu7#d@t({GK?7XMmqwYkX?W20o6XUT zf>wgq67J`xWGQhxDL<-oL;_QZ)B#!|(G`2B^~bvbSq@;q!&jB-{(M~QJdnAez|3~r zm$frIgM2F0c5cG)D4;(yXjx7KLGej47gSiFFzVxOBEdhvv-*@+84@ZC*p zbiu-~?)P4A+o5G=t3`_1OO8-tfd8te`PQpKnPxHVW2icba(+lBqx-rJzbJ*=jI4Qo z7i!C`TaM@+P`RE4(Bk@PORtRmVaD`PKmw%-2p|Gv60DZ9SnJ+xb*(R%H9NIU`liDz z8QQ%hyOzR9f8?`Sv$-9!zlTt5o@<`46OBgRa`tu5C^_qk1C&CCDBROn8`?lniU0yg zgtg^A)^5=!ErssaT>6wMPIc>Zt*vX`E}F#EyxeBp3Pi4O05F_weTr}OqIdbS*}7rf z%`>hiep5-9ZF{{Cq3VQFwB{rlq56)&rv(U*6csXGY@e59QAE(&ef>1HQ{SHU3ix>}hjnh=QdQ7)uk#XASS0U{qv9l) z07P5?JBP&aA{}oDeSZqIt@B0=T445bc#u#((sFMYJ3z&mo&Dx2`&H6A8}3aC;ljzV zk~8TW^t<(`YV=bp4noS4;x@N{Lw-FXbEc^FG=L%^lqv4qG;VQYyR_Ul`Y^zlk9_Lm z+Mx}-8QnK)HSJ(i9@jmn$czxkpXoP^Jd4TQurrKTdHBNDR31v>wKOE;`kG31WFSe352GQQPwaV1@t7+e->k<=!MVJ6*qeO9(NP zM0$0i60tHO1ID9%v?F>!vrAambNhz;X2}Xb_+QV$K|wqzueNSIj;b1E%z5+AFCtzd z>lnm3`nCkar&Q=xnWkWast%&ygwIKG-Oa=AR=fam$6&N>87RdpF>uKk@%AXai)skn z?Y>vL8Kib^p4koTc7AT~hF{$rCJG$~Y6thN>1Vi+Do@5+{XsA8)WUw@b#gn3Z>G7csG!;t*}B)kOi?Qb2Rt1QQu zk~d;VvQT@Xbeja;6`$hDdaj>p=|XiQH_*Qo=sO>WY@RKEjT6TGCqE1X0TtfmPHL{f z;A-iwmu%p@iM6TtV4W68{el)40Qg?eg&a2Edj>2o54m{v`x3G#stsr6Ts^F}xySQ=GD|Eu`a_dt)D!TT!6W`~m$ zk#2VHY3tw=(a`IN6aFlhOXLRukdO38mkPYc0T0TY-=|M=*zo{(WoIBW@WGVjo+WYf}oL%W>l0F0$?T@YeBF7Mbes$R`L(6V^ z5)DL$4*-64!lB^L@!$ifbPVYnH3$m<4sWfXnqKfD0BLaIRq zjLTh`1a#fsNGCWOCtr*Vt}3K+p8vD#lNuNh6a^YNQ^dYk=7=T8DkBd-q=QIY;{h&u zml$mI6e606kML>=b5K9sf9A{IHC(V5yq?>2j=N}sz_e-d_Q2$60AU0Qodev+GK=8s zQ6$yDtI^)?UVH_ZXno;!j8BJ0qy{ZUEnjA~HiOh7B9_0WTgx*@^wa7J0QHdmr+3^N zKTLr1kEWT!VQoFnvykUmXt1tJ{9eEA(o&H$!Y@&ub#F(3T!+150D}=TJu#+^UH{?( z1lDF4a>}k`=_ye#9V6DQX4T!~l8n~T?fH4mBulGi4k<7WuzsQj1r!Q^*o-bvzNDiz zzb>wJ)FY+i#V3u`68TBLkmuUmENj+(QvU_u*?R`d^(w9U7XGDTvWKg%&>NiG7_#l% zc!}vP6aKrkK^z2AHky~vl=bR7Za#Z)y}<@~+D9=i`0DX#n7n?;Jl~RLlih(-^@DlN zRFzn5;^6){k(otP%Ahw>c)0ur5l(m&tU>AA*SA_rzy%3NNR}ISp>aja%7H$Bh*LvI1UlIsFn*t21 zGKwy{zgnqHbjv0BbJIK_q_82oKtc|CYCA7XYrXX9{kH`fo$53m;YAe~?;Gij$CXju z==>wrl;@obeSrOpsf|v$ML;d(yH5{kd0^&AFs&R~Vk_KrlM}0OXIa zH&A~PT(K#G_kf-u;0LyjMZSZec-IAXirf7b)IT^dRzY3{vLQEX+v&?SvyFtGIm&Co zw?X=1c0Zv_rB+Mtf$nt+Tw~8`_&zglsXIK8oaJy7 z&IEsWl;{`x1UJ^^{T0=w?xW6<+i>=xY}NU`1nq|riLq^mzL|R?W&eq0q-0uSqND6i zA38fFNyN7Wp||V59(b(IsWKcM9?+bUu#tP`(@N{Y4b2Hd0)s7px(Ue2&A)JPE0@+D*h^uFA69Z5>CMfgyCoGGFq&v& zoA^NtW73n9mT*Zb)5i0pXxi>2H&6Y%_1op+5Hb3#xolrQJHK_1fD|aQQ=fTbKpjfy~Kej)%@_wQ#8!N0z7M}-s(0al`qR7X-`DvaaSzo z*oGAMhwwo$aHk5gw)6E3&=mwIku(e}$Y-{LRrY=^I1=Toy)Kuv<)1Iz6`o?3$9Dyh z!~|A5scFZ%PdK!~3lqyOuNTJnk|nE*Eq|HO&=Cd389-(Wr_R#0_9?DMI&pUbDtw+4 z_+Vd!Z&I8+=SaP+OcRlFgFZgyya89w@fV=b_{|Dzvwnio?qaO*t-F^r=M_<1xl?cQ zeH>sM@gw);EncRKi-?G#@FCekHhB7c%%D7xkd4vW`BGp1&WLl|Zmnb=;8S$6%x>Ph zBw1aY2hUcIRX5o6B4RE2jMo~eYVTo!U`5~YEh-Gy0iwZ;Rej0e;dz}wX46|%qF>Cv z;qiS+t9=!oj@UZPO}_!&m)K8T@-J9#z3IitCYnn)du}o!_>&XI%Ze^wFNy`pWFmWUU!Yo zwlm-Wkm9H^PgR2QYPg#EL~HNbiVip{RRA|7FXS=yZhJ^hor?=Phw=6w#$oS-%t-0N2V*G`rqjG~x-h5><*2ShuJ6h&o}=;QJJh zfve%~#788*?Av&KlWP_g2Zqjrk1YO+`|WOzH$jOg4bj4Z2Ofla(5#pE%f!lH+vLHq zJ5NcE{Mas@Ai#7Q?GQ$laH}N*7&u$~@Zb=tSDTOQ0+PeWz>+E{S>aw5?!eD|_ZMkw z7**E(w4mi-1_D6bv{2FKl~uP5bZQ-Y0aD_hqiXZKv*`e;w^zE!#v8QRc^ez6wh?67 zJi}Ew!QZ*isLC1o^X-W3XY&z2fd2Onjlq;!??Sl-J5mLD-@b`5_w2;e^3sm=>A{4s zNU8^*imixh79Hgfx~A>HaceNp(Dyf>iviL(82i7d1Mcl}LoUB#(E#2MGQ5-oiz%W* zAx!DaT$`<`fMyW_{vO@T_ghEk*{~_}y8bOp6bJ8*jJN}J#kh#!9@9qdpxU^|jQvcm zK16~-%t89%9B%eh3C~s3&uLg!7~);Al%1V0$qRj;7I2gZ)e&U3>^N1aT%M;CDqF~X z*O*}BMTbjoIp>2TYT;H>Ort~lX<;F#XYNjwIHfYfnXlX68G-tjX6r71%jddiRV@mA zs#9wiIb>r=xHTw-DfCbFgNE4Ya5oa^F>-0l2K|Hn*#BNl$A@26#3@%A5g%+Z(Tsk3 z5eA2gcq5I6{~pvCvk6V?9x2n(f1ySqe*Xk(F$tOmSQ79`l1;iR;BTyg72#?LUUIG0 zir7Q-5kUg=+y$@tkG^4#{4t^e$XAcs-2OBr#>rBa{*?*jznE9HI5(kf+K@4PNW_2r z+33H(-ju7YXbN!x;TbKZqiXmhd_Zp(DCNr?B2bji51FW8CW4P;Jow|!@FSKfLl9X3 z$btg?<98v&41%TEJTl18GY$nxW}1d7fmBxy@z0UHPcXPoHHqqE78~O9cN%VeIg$gN zFlIwE@mr2x(Zm;gRyj;>l!?h-jW-n#?<#ICj4@f^oKb zz*o)Fs^|DL2_jNyrRRM34UM{mEM#b#F>*jcwikt7$RK*~jar@Kw~q1JEEHV;aRaee zsF&r)*`4K_ikk+YyFzG8eg=lTVVF;*Ql)F0eMaSLkHeGPh7I**PFuRploqGw8 zrH;RoN2L|4)RhHNZ2NdP4n{4+V6{>!I#kMwNfc0LmhOIEj$NO?i2@8u^uQ20*T2XJ zmsKx(-Qfx_u{r_B@<9>cJ8CEv>074iUtuh!WyO3vvVBE{AB!>bNv7f`}#PDDyW zXOH~K5mdpRsO~;&uZ?+4FNY0oj{d87fcI)5Yy126>c(;ik5A~7Mb_f0hXaSKf1>`? z%QooIi7dc8W4{O4U+P9u2VUf0V0DbNW9nQ6Fo(b+X12ZU%VBAfAF6R@0>H-$*$b>b zPoA#aHyFSi4kOrf2p$aMJmxOQCf_Ltr+rBu+-Y6kF2_90cpaD-iWVBe4Q?lANl_*h zU_k*E|F3-}t@gy5Jp)1;ajWe)VgP=3KeSeK4X@D3EAyLA{xa~T;qVY|9OS7Si(XKo zdm}hVyahAhge}@)en}4973tq;<5h^^6dRju7xpV(0@zhxl$Q-k&~`kCO{Ws}5v-lc zW(}ffD~mp~G}wy^JVNZJFtga+#7?I)RGQe+Hi{8XaHb^Jp*Z4&lVz95Z(MO!VM$Qp zrVO2_$FukXFMhuiV9@uK0a-vh=TR2;A{r)5Bn@EjJ{-)xuV;G5p&E{5S~+WD!w^GE z*!oif%w(FNiA6NY8%xEfd(XO}e`Mk%!s_fnz_k4p-DL7t3&ao+*p;ps$K`*5@jvXi zCNUNFynw#>ym9_QP!iO4s=k)c!wj%K05l$h`iL06Pnt-1(fG(59tHf6g`M}<>0hZs zs^8=HNDpNfTCN;TBjrf~hwg-5yX^~Mo}KO%)RrsGBfsYMpl-gCU`a@mDg@9L6=n~DO0=cfdgaTWju zw0a@{)HIh{xIFqq7UYeF*<t@yqog`Ig0|#>i~|yO&)uV@7JptpKBcdkh%F9>^TM|5kr{P%2}RxIanX98b#B zl_LN*ne$s^UZ6%i+!pWL20T+hnI;LyLhMhdEf1zjH1q8{W$ZP{@WrvaSDf;WmNehw z|NN(-?EPYL+#U|_ljSKvK+=&)hZ;N#6$Kf9{u4`ESpswQRo{gkxhdthVuu>3bz0Ya z?f?En=RzHCIDd^3$Sbgj=4rJqS?bwRV794lFRtA)s9TdYGM}^6#I0X#TZ(;TIIHyN zS^ zlI@!s93UW@^$o&fW@g3!jh4R^zyvH%E-$8HEr2|jRMs0!c+Z<0S7Hzy+z%nhfY*wOva_N`88W zOKesur4X|0uVQxKG6!Rq+d1N12IL}%%34YlHmm~LE$j{f^Ucd4WORTgS#!zy6d7R# zcmeuHgwBtfgtIHoAvP697H{;xgyangaepTk7^7BxJ&%iTL>;3@i3zP+ORPFJ)!V&gneqcBE+^ zJ6TC$IP($$$b2{@{v@hSBs?$?GO(p-hVG-4IZ1lBu12c)ZV6JSp7~d#NMhwxqix4 zT6EZZ?EbKs&R5H8US}vP)1kYYfofgm;kM@pVL?taAF_8^CIWxej6=a%OzcLWy9$pVHj39X+1?WRq9tH|UJTlg%ayVMhQ^#?$Z7-~3 zhV4u+9ITyR6ECrIQQ$wr7vNxYu43uko<>sfXc5Dz`kS)6eVuZ6z$0bWIsdX{k`}j< z6>Y?jo4#@^WZZvh3{iOF-JcLN?iKtVJ`EZGCLtXNpF3OrX9G!ufh7jG=eT8CF@o&9 zzFTT@8OSA$@I&$}`(JFgj^Fa7*-MF1HZmDqwFS@)8#=&&kp9m=^#({7lE6w?=N(R1L=SKQlOSOs(dGIWC_~#aijP z&Jn^|=$ZhRivp<65p$Ts#(?+Z@(f2W6TtPC4I!l{u0HW1NM zieHGKr%bj75Q)jpMEK#?sJ#WcE3<9ws0H}kqr;WQFwcTvOrA5#j}W^wun|+W2%O1H z2r7{HmVaL}`-uo3{xw!~+n)b{ue1GDr6RO8@a;K&$`?=&P;~C~4-Lc4><3unvAPS4 zqn+uOxM@z$DZ5I^EMqf2MdGz;T;60b`<**77qGK+tW4}%AuqJ{Xn_F&>BE?Q9KbC# z1Q$V0w)edN?S^K%)>)oG3p4ezIklM|qhE{~1y=#3I9zo2DPXuswh%0*TA24sfrNgs18O&ajkdOMv&} zA5U7bg8su&aIC&dTLyA`rt$ib9@NO1mHJofdOJ31K0Xvvic3RsSpRwLt47|gGl=r`j7qm{mCh zJT(dss?wPMCzA-!tJe2X6xl75(SQ!dy~WV)&nl--?c+)s?@tm3b6^90ri7osoTuDe zcXalpZqnHpPq`VLX|Bv9jQ~dO*2#f)dK6U-kf+bp)KJX3Z{0 zVd$bY0xp-NL_6`NTIZ-v+w{3aW+BI&$*n{f;f#|GP@;62=EwYkXG3=bA8^0)Q@l@OqkyDW!MF^vdU z9MGY5)Et@4diF#SxAJhgxQK~)Ga=$gD@soQP5838{+a-i@ZTNW%ToarmwFeA=CycG z4y+%(7Rv|fvVdjDLtfl>E*3Cz7vZZ5(GWuVVe@aFk4U8D$mqYOb8MT5$Z!~ASIGaU zX^ywHmXp}5nm{-IQh2GYa>alvbSq#V7R`k#i>qK=3pneWw^yQWnQ&c_Hc6=HjZo6I6}Lr$~V0gB!;>uI79F$fH%S!NK?f?235VLV^75&HZx(5I{gJ zJn${|c+;DeDpLMLl$3D(fM}y@Nod{c>lw?)<`dfz>@H5!={HntucZ>7BLSBks2%n4 z3sF*DzAB31;j1CgIB&IxXn8h|*^Q`%ZUhl^I86J!{YV;a+!kBcAX#nqbb3R@AqtEo zYJ?XqRhqXe`elDVuh*RKd^F3}$j_nod}mP%SD07`d`CpU>$M}3B)2)Nso$)?NPzp5 z*m<|CSR;MT{)vatpLZX9V}btPGFsCfdJWX|FmrAxwHqm~Ac`q1{%!NqpIKu6PV40I zFr@-q%-tJvc&XLTsoSaCc3Yk6vpcr}zUu%5!z(~CA*o2Ma*jn_$inRTsT}_7M8vl@ z45*I3f9vR+8u#*1Wdsxl=X=FDDzWwFuzJ>IyV>&!7N5V95v=xac386-8cWhGhu(J< z8LLNV%URGD=IpjIhAiKxisZkP0C;P_L1TWffeXhc^q{}!K8Mii zS(-_`-jPuYx$+Kc)s;3SY>5;$G`@mN5W2z*_1RUi>c%|0`Vq`H3zTQ1)!QA083z<3 zK_mnjn_5vy;WsJVqbU*9r>_MdJD6P z|94WXS^!3h$=#Z&*o#-Y!gufw2m$QX6RO#hQ2Pe&EET>y8&SdZNzLWoNc8if1!0WR z9ZjYAzFaLXmr!QwGp$pqjJ<$0eDL0kxf?a#+vg@wG0}UWjmr<>r$j_uAq-xoUdanv7R})!Q6XnK*Xbt;1LpD23BdkPVWoR0O@Oq~LJ|$w0|or%x6ZEx z^FBi>W?DYxGC!a7DG>8g$7&sCP4_6As$`G~QtMd70=suGo4#f(im_Sc@U2RMFiRy+j zp}ft_>ERl?vcsr$Y%ceF7=TNYSGG!^fn^_&ztGFGN$1fsx;%E0r$KlTj>w7xq~gw~ z(c>xyCp^&dWFKk6x^qLej-da*3E&%TuET)MpJ91+i31Xi@87}8msdXz47~G>?8r!O zU2$bw@M8JJ&qbIG+SMcPNmtr zG4Cj8I*G$?`X#YKn1eksA9JLgs?7SSeEQlb&M?jO*5oFXw(>z-xkGf+MN$P4lsE?lKEdn-pbR@$GT0#fX~~_%lZ7RxYPS}?-x^HkyM{^jj+h>g&d`; z6oC*&Hj|q z7(SO3jwccY>YFCAvfs*SJYWGo|KiQpJ|Hs-?E40_UR1#qZ(C+I*-HnhNdW2w%+^AE zz9l^|$O>Rqpj6CNCKw$VD#mE|g>zs>@zc;SuLkUGb@4{`9Fk?z+4+ePFukHaE6f0> zy3N-0{dqN=D8vUqY6YHi$W(S8ZjfalJE!L3B8BON=;eR{eNFf8oS<&UWP-&1BPm4@ zTK1ovvhZ#ciQ&NW8VxtzN@eVVSHy_S!KY8a?bHI69fvbhMxuxivud5z`*0Y>6VcE` zj_HjavLOb58p;XBR68J*N~*h7uhL`HoRrO86;koE5B8$~##py@6Tf=R7`wEr?An?Z zsjWeP1sdCS=vIgv9hMoixY&FLm`OF_Ymmk`s5EJE8_1;!*I{I0XyI=Yz;QWDN(@}a zvL|7*np27(7oq1#`CBBiz$beP_WYtMZDeJTaJxjFE{ z6T&~Q`2jqr6(qLis#tF)2_HG<1@jNG(&Z||OgmEBU^C8#Fgit!yy-o5b|-720K~j| zKvib`%^(S)DxvF5FJ2+Nx5<)&TPX*M;ldZvd76I6&s|xk6VZTVz{NyM=&(3^YW=+M zxXLdC+0Jw8aP7b|pb>cZ2Jd6)=cmve5#=={P74pVcJk+ns}*l`aLHIDhvbMne}55U z6-R)SRvr3d6)+KB5@Ny^RV^3-P?A)fi!^51?m&l2r?i9+aA&$~`}Cq`!fxAfU0X_4 z!60}#B!Ogetrm|~z^^Z?Q+?3M6}!2$ZB}(lz$PI(_53v|;J%=<@x;@zKX;zGdQQH; zL@Jtv-O&IyNkof2ZHZ zA8I|9R^EK}ebRq0w_q>yH25hm93LTu+BXusC#>*av*4ppS=X2GM&XDsLZWn35pC$A zzJ^drH-Duw!|_th_G!89WFeu36jA>g#)%7Ur~YghJZjOZxCO? zw*Tkw|G#w|kt)rr*W3&WF5JK6U?W^q_SzEsiEJc02!Ap8%c3z!E1?uKY-$84GC%7p zkm2l=XZ`McF95%Nx~PdZ;P=OhpZ zs3o5z4Sst+Y6~5O@>%V-sFi!3#2HQ%nIgC!oRQ=g6ePWIhn+^X36TQzV1Gti)M>kb zsZXSQ@4fJ3;~ObuKgSl=cPFRYKP@bD&C^QN3&8>N=e6$P+MQfJI5IMgqYt*-C=;|@_%nDL;QnWFx^$vo3(8k|wkYgWqC3I%RN=_Q zW&U(QXu$l04SXP*;+h0hzfxHDgM&gOr6FKjyl zj(fVj!>RS>PpQy0b?CvwuNlj0iVF=D(W^$}^Mk^SW}`~wrE+yYgUqMJMhh6ccFmp( z;neJ^7moZy`s%d>+FK{<*(Gnfk@Lka9~=G=9zBfDu7^!T#6TZ>t7CoDr6M*(VBV*Y zNP!($pd6r}556zjar%?nu+d(sz$sFCSXFgt{uO;@Nam{mhvp|X?=4|I2}D5 zuic1&#am0;t7uIjpWReFUZ-U75w;0SR(c-wC;Sf6M{6u4+7}@~(;2+pC19p~Rax1H z{bs8wF~eqqxw$#L8hhX@^n@fN`rkGr`ZyFav^a=}$qQ_FEVp~!yE*)-)1~%nRBc$A zu9a676coJn+2bD^7(n~23Y+;7`FLZZVcTS6iQiJUtSN;944i%hYQugFg*<=Kd z5>itaZf!9;7(BS=y05L8)thfVQ%Gdt32cI{B`-K?*UzQ~%?u3DG$5ROE{i%k{p`04@t;9DeL;1Sz7RTBnAJo4HyKCrJ z7uilmY0vCb4h25nEchWVpBRDNtPKFV=BfaHQ69(G679;3ce_8!NT{e78=!fov6wQ_ zVLO}+XUF?t3y>$ex^pG!kmStQn>I7_3cv_ySy~oEV7FWm^3k2iCZe@fBu08d>6-Vsn@8StQ`z;#Svf@bP*QT17Y^Ne;PymKx{0=8x;}c_w z&H7K7bUy#+fQ8ongEBpv>6;}L=;=tv1@GDVd!K_nev4Y@=ZuWf^rNH1Z@o&%e@fyh z`P`3V3Jnu}xAMBr9^y5hCidyo?-IlX+SV0}`Y9Wi>*{}V^73lziw)2qnXV-HZu&RC zb^}2Wfxr84ERc{yq#k3SKs)aLUMhA)oF6|;9sDtn+`pGB*D^!}YdI@H2IkzZhhV7G z+u2yq3|i1OcT!RsZ3i8P`Duf1htcFiPi?$~+ zU5><-3uLtD@jHnt7hm`x=U}9lmMF0MUd}X4w>nCE1KYpjD)N63T=@Q&ckjR6`J9rW zkDX7*Xa48yRH?T1lGCkITAVr+6n4^};|(|Do4qyWNFmt2ATdM~lv2kpdSW-gJIf@1 z6=mfPkv*BFylv|eqrdARbND9f*V3lfsy1QSQnDC$Q~GLRGQ+90d}b-NkW8IvM^!EH zzmfOeK~X*5z9izMovKW||y}MU;ul4zSR`1nakpVg}x{$W$eM6VEq!aEUc{ z{A^&Qn2|Qr%W;Y$f5npD44K(36y+zd8J6voH|vC$$;UJp`Niv$7y(RMbTfjpbuyR%rv>=;J^u39d<`bgr_IGLq(v_;+n(ORKBgw z4Lay=p#qj@5V}dPXzD<4^<9@SzEjubL7o}QdI_!dQrHG7JG;tG%cTyhjz0wGosKEW zDzd$!W4vUG=n)>C_KpsF?fW$=nMu%GLpGogj}zD)R@x!x3OE!OP8No^t*~+yY;)3%1@W)j0NdTYs%sSp?G?G zB0fDmcf!;+pQ5Z)m2ws~=GeM7QQqi5vSpfUiP!n-&CUk3AnRcPyW+J?1Ua2i3Hjk6 z5JX4Xd>-ud5)T0PFYrdBdll(d%R=q`)l~)e{aNw$Hsto@R<_PzGVjFf_i8s;Vl!lbfZgxxMYV zE5{UJP`E+u4jpXHxZk>y_1`S2;Ns$1dz@$rR>;MQ$un-Z8)XDcEKL*rW8iSfo`xJDnVA(N1hp0IFzZ(kqaXqkBiGvm7j&?8IpwiVT4 zN=BC4`}_B}V=IDn3>3eEYo?_Yr^9B*HfC;n#Hx7=pV@G41V)m@cqZrb+3qEK>uooV zt^^XrI@HI)k7A^>~!sGvZ>PTftCjDzp$TeyJ9u_%kEeT zR-Ym6XJ~y}x_-#?B%{gd8`s~M5e>6(aR0Z+V6XZJCcprq78vk$9W5ppyOE@ppa(`=TlccbfU^!;Gkh@5{n zmdiVjv}oP(K*hQo=lS(OB=q)19+-MQI`y*%Hl{4E*y_A=V87^}6Sz_jEwi_OCVF=3 z&(FUP_6VDE;HOdBmlQbM@H_F$yVnCDF6TY%(ttstDvB4@1jNM#N+;kxx(!A3***9o zC-6N8UAj8uLohDhRfNg_1*yw49{oN)zukAao!gDW#%K@mwcgv~WUAzQ%!pcw8>M%v~?F?u6wmBL{tMk_2-W|I3aoJ~yKAd{>wbksBPyEjUs6cajl)2}4 zi)S9U)}o#@?+oc38xN{tQP15qDN|?ocHL9c9@}g`psH=|hkRTBU4C?WC5SkK_;2du zdUx4O5s*2I*+tJ1k?$_Rca1a!VZ0~3*p0|Y{(Fnvyy8?=)sKOg{27SJ8_)~Vt;u40 zMG?y)1Zkp+3w_78u6f!{t@t!4Ouusm1X{((1QM#i_VdPd1(!i%U$Pd zm3eFLl2_+Iir&!Ip0;j zn~e$@Do3WTfUf*gRm979+@$b25E2>N%2C}jCHF9#fXwgRHm~jQ`_dsVKo^V+1XCDj2g4(8&O6~dh$7aQo=v2g}SnHk$88FqQx_pVxq`Z8viaLQMRpO5+F@>kP|}u=;PWr)Z<)&_-Gi zopl4TrX~asPVZ_v&v6X)h#bvpjB;z5I-G1;+e%&8WqOWR$Et+9Xl|%D!B=O7M)@f~ zLeyMXIc9SyW~bde1f^H7gllfUC`s-LU73|sjo-l#5-3BYgfE z5DXgKafj{?F)-FRv59M$l=M=YQ#x+GgP?}_Se3Vb z%HJ{Yxh41BmS_|hz|lC5T9iEBeRg50_Ho?bmU2wOZKs3tQK(dpsH|$ndDSec=Z@o| z6{Vj;h0P3Is6*o{q@!DtQm%!V7} ztyhgjLAE6xXxsC%%0y06w$lb;u}?;=p|)Cuor0o>l0uJ*)nRT-;^_AZGhDxXd0Ve# zar|BY{|Q3E-B<(uR<_Q~Rn!q<4yiOVuB*R|fssZ5D1A;;K1-C*kLgm;*J&1^=K zgi0g#zCn8sJ}0QUDz7Z1e~iVI0^9#{o{6Ql7!1z|Fi9psvlI*=zx*D@c_H-6|6}Fy zUjkUAx@Uu+@d$H+U^gS!C$0jEAyL^L13Z>pJP z;GJ2W$DOhi2K@9{+JDz-Oym&&W7FhjJ3NdHm7X2wkulL#X^9?k|D*-&R}idUH1uHSwSG&+NH_uDq^< zg(c7zjMHi34HigMIe)xco}cMkh-cS2+Kx}W88zp?-Z!fV8vSJjMzq}!8KbL1W26y2 z|LES-Xli=el%JYGdR&86IaXxx_z&w3dj|%_3X~G#Eh5pM4CHw-j0;eh|4&sd-58F> zlc&vBXA}lkYb-}fgc9UOp0ghg0e_1(v^f}YFh_UNyES>Q4 z$117UF?IGR^(pwcG+LU8HSyG0GQ*TA1d5mY4_`60@5WZFEV}rw%}h%bL}1FTG1G`%_9k28AG>Pi?tvuZ0b5 zN!m?rL#SYOTUGHz+SQidQIGNPM#d9O$AO9Ewh*~+dNjU8F1fETQKBxbTA&H=zdXBv_EB%d+wjJJPliUO z3hZ1QKAR}^k$SbGk|L%(N9e%>+{j~rS1*&pxNLXVo?XB0_4n@t;Smw+8r9{_se+BK zABRd;Z?r&9PTWfM8&yR;P#Pb;3X~f3n0uq;J@j>JcUmSQVj)aI(@@|I7B)WRG0XnZ zFv6|bA2!tz7f#s@t)&G+hp0WolwGU3s;cTMMtt7Eq3ClpyBJAmXn)Xi8JS^#i=nu0NAg(@-`*V1zDs_JSm6SD$UboYjTH$X=ZmOdm~N-abrdLk0- zN1BoWMVSdUmkzIUy!8iL!*wQ#wOO5gO~Gj2@yhEF)06!~puuPiG*Z{*uC)@yCa@ZBX47g{C&Emmo(T78==;qVhLH86orPj6Sv z*h@&8jV^VBN0ypc1G0olO~9=brI@Gu;i2>At{(uir4)A23XP496KW}MK$|8@3=@!_ z`_@KFgdI2G9kR?TKu-Zig!+@wt!(&82vvH-wKADVfG6wfVlV3<6IM1HVtn3 zc_Db=62C200tA&HlvHnb_#7eYa@jrL1}DZ5>C)%*ZHr zE-9p;l41k_Ca=xp2{RIWP6ZfYeB(#HJ|aL8G$x9(I5wWhxZMnc#|=1WIyjWsc6BSo z5OeAm6^po!kN0pQp9=~Ks@?i;svFO(*_IkL$iwD+W8HV>_3c3bXpskRPn%?x>*c89 zM-G1pr5;0EoiPJbr6VMI7%jCHS!7yYYv%I%t=l1O2MqqMIzTKpmz6BS*_pyQsX>oJ zn+-1z&)y*se;ut5{Qmz1jj!_mF2x+>hIvN{0qEr#9~a$ZCP`;*0d{X@*+Xu31Q-M; z>HnM&aJu;chv^BH8@|~X)}radAoOS73R6m~tCtgW(-D0T3IH+&j0iG1+`q#18knaV z6FEsae^N1RJS;Laadc!v#(`5cayjFbGnW*~L+2=T=zsD<@$fxz;enCuj9`6E-#5C6 zVw=R$qhG7l`@V2VbQeDsAG{Lsaf@rH!A+pUBhSp?Y#sVece+Ci2?0|!K;kte_g%LB z5Wu{_4@ZWrG;96s%AS&RZ9!)k`Lg7aa*C&G-gzZ345%g2@jE`x>wX|xr0y5`m=e5G z8_j#Vw9Te4N@1Q`Bh0M^;`As|2%@3iceeQ*{0t94nY}0&q8PIt6*gF2zEhElw8-q+ zkci`3|Nbm`d$PuE{*OGrw&U$iA>@9;Izq5((I-f(QtOF(;&uJ_W=iGqq!NkC|s zspvVrj@3#>*}~0WKx$6TxnV0)?sQHHHpERa3AWz-BX6AXB(LI$bi~vLyI*-);^H;o zLnu}oadh`L${%I2HXyd1(Vr-f4mC!Elu@Nij|V7+iwL;(TE?#Xr$9N_aX37h>ZQM6 zeN|DU9DTz>Q0%W0 zjW)mW{vMv^RtFM^Q6jivIVQBxP}QadM0|BiS6tqA)XN%hfs*c-iq7kyyz93cj<<>i z=z!67S=?@g?WcbUs&hx}Yh?6|f5qnoyI#HP)MEZ!5v?kk?WeFvRuehrGC}u3zIJ7I z)@L~Pv#GaL|D@x<#0EvxM0T>KB;C3~%%X8}KD%gBE8_U#uv){;*<7G-=j{qo${W?Q z`Pz@OaXS0kJLwlcLo7M_pQ3IC0xvry`6{rKg=i?c=f~6bZMUp+s?&wy=^X(1T6kj++ z4`^B=%XQ2MFM;Vm8PLWBk&r@X@y5y9qyluev&xLcMM7Elrr%z%rfma*Wa`lW(7Hbt zN(FWxFyhNf4>3J&j89k^DZE1v9?s@lNO=C^8MV14@agb~O1rLH_0O}h(O%G?qAcT|~7{o&B)mW$i|m5-nqa%@pF}0*;WE zR0G4jsf-~!I(C70TG~P}Cf3gOfEVh$n;~DCY;3%gz4&A@XUL4cz?08kwSghJge6ff z<;>qZ`<$!g;_RWgzgnIw-q-QDJ|6JFmu^Io4H-ltRb7Q-!u|IO_o+{e9KM=D?A%Jg z?R6F~dlMWQP-T;G{*mpWcx-P|)+^K{bJ~VTr+tiOsqWBI9=b zwKJ_UB^Pv1OuJ2b@s|qy|8m2IHuP@x0>hrc^YZD!wqAf=SAHZwI+&EWvqu3wy(q!? zGm~RJN;wT$5Td2N)0ci3P)2AqWvBLrdD@oXL;3AJ$s$j`NoW zu|m*{H|wj`g_c@7=Hti~PK^O_tRB9B*C4RnEWd+OUW%amwr7f5HHn4wO*k9gmEn(@ z3C5SL@Ul)IH%S6loGPZaiu+Mo5UQh&ppd3laP5Ax!HE#rE%cw+U~%?|8LudlHh&26b2 z?&oQ4A1A=L4&7T*<+pi~_e-1uFZo*zEpx^gQu%_SI?vpFv@K%^X$`J>OwEdn#?cps zn4p-X&lkucoVT0>Zw+IEX$B}rO1mNXwren9nMpu@#$c&_ls@*zojiqCMA6i-S9|WJ zr{VIW`6bA`F`F#6XgiWV*>-|c|2$G{q@q=3GX{q;<;Fh*h%#Hr!i+V@#^*qA(|A2Z zmudDkMs`S96L^Vgn#=#FMmc1PNc7{#oQwU>QH5*CuK(5i{EIjDx*&x^x!wF(R!J!W zxEjjZsg}`Eqo3mSZC!P%GSa+Wt33|as+N_q(C8bjohc#)6*~JNE&%UXTR#uQml$Fh zJxA941R|Nk_qBj^M`}8#Qr|mqNls(R2Xw50Ls~<{(rQ<{OLoep-Me`^vqH7D((Ykb z$1_O#+NN=6;g@z(vJNgKJ#_piyk>C=sc`lS{S<`Pd=n=4dyR@cDZRe1&Ny=C!PY5b zE>5ZIOy^@%lW0YSHu+5*%vL%|c7YNQVhm~}*M%gURKQsX-1mV<%6T_>7Q!-)G{2bNpgl-`$k5d zsb%{IAjlzu>}d_Cr4|2f`?+6RuGDl}3-S81RUKDX645^0_Oo1@UnK}TBbj?PxN#l| zJR>om%D6{w>YbLhtPWro180H!Jd)>$rJ}~RBO4X)6|@u7k|iR?rhSdKwo2Szk%pv` z3pMT?lMd>gJ#IjFpRZ3x8J|u8(L%i@>lso(bHYS-klGTW22_t#%!oXn|J0-DGy?fAveZnw4()EiCGV ztOr*fsR~MbD-4)-QT$JVj+ftPjeHQ@D zntYoNnI|Y~U3ikSm>aeA%HR)$CyPq-7-^+%~@l|D>-;vna=v?OvIGW&N*Hb^C>WF9DXLB^|Heji9=)+5&^39a3H+c{a>OBJ1NBiX+e#yuVx5Y}J|0O`GN>#Y~A+tew=G%jMM-xXxXYnq=-pAZ_fp6( zP?YF~s{MGT=YvU_UFdV>%2KwA;x$-Mer7&f1UKtwoFqprXp)KPmN^f+yj!HcP%9e?^`s@0?|$faQd^gLW_uJh#4o%(04$zj0*E!jN_C6#R&i6fAz;JaFRw_mt};~B|8bGGSJ#6fgk+E^7v*|!4TBIUAsN*z zE6Qw^R(*TA0lg;r_)1R!NLDBg9Z4V<`8%|{EtlNKx^saO%WC>` z+LKQwbaY;}+iy>D=S`E$T-l0JHsb#nuX3ysxc<%hvRB$>2)?k;!RJ2tu(pX;UlO%E z`Xl?hm6)Z)GQtPQM!%iW+pi@I@F%;ji(YmclZ@Vm1vxZx3n-=%9*>{76r}KBYb;YG zhA7LAe2+W2)xDwiSc4kheoz=S&W$FqX$8Ct)(N)ehoo#WY76VLc4aho@`jVLD5Quo zh_pIRnaP1Y&MThI0pV5yu)jF}RWSMsZbYfXY(t}Yms>GSoFHmZA&o@F`^;>=hL`zg z$ho0^_;FR9e&Ta9s-a3vD2j&@`@I}BEyEchfp2Vj8jP&y8O;HxW3$fg_1a;Q$hX~* zI1k>MO;$&t1MQ975%ulSQJ>_stV8BIlIc(fmBuc-D6QizRZ6vE>$4o(0n_jKvz6|C zJj}AW5Xe@yX9vsOV#*m=wXsqbv7lCkk7nL!%l_XSL>0CsFR&|IJ&+SP$ zQ%h7qIk%r5^|hZP;CoppEU|C%{%ff8DT&w-Izl`UJ#DRacGR14e}TNp1=QJ#XOT_3 zugzYB0P(%Ab>8WZW_SEBuL6MFPx>Shhw5S-_Mhb3G1na57+E17=2cdq@HQ3XsWJ_& z+jraQZRt_mOx9w;Rg5u<6En7{CTl1*EL>-2CO1;F_3;dcO?JT;cQQHiY}--aU9gCZ z9vEn(7r55vFF06t6&87$D?kiRl_vdGH2HQN2^v;khn_Ju^83Co>cR`t0_H--m6i5@ zK~lD3-72!QT5jXXDRpd4)Ib`LX06Y(EK=(Gqy$!)4IP7yWqwDsPe7A3&!d#oJ4_TV z&A1>%D%EmwHa~mWYU^3`)dt0~86U_L>E~tzo@M_yhPi?2iexBkjo_lP6Hd|~4v+1i zp@@vzojI%xY9&*1WU(g8jSs~xAD@+5T+3oyaUCiae0;B45iF*J4EWfJ0B}WH{Vx~Z z;G3pK9wkF}Px#cx#v$(&$&Kk*eedoJg!F5EaRA@rQhkwwR$ZyZ~fTVN^_Sj??U!ID;P5+6F%drtTl3_sy@YNiXp!D`#-YEG{r&fGJyJ>s|Y@Psvr-Tw%W8gXEZ) z=#VpnRjggU2Cn`b5!m?M9#yw>GG1jte(`XC=X%`YyDZ?x(7>S6R-^w9*d=8bq}3iX*k62<%wbax0R1#QSj14ZK5I zqPxgCUtkL3Li_d>wdYfn=ob{$KJ8k|`Lr^*afh*2VL ztt@YJjkm^|)e4x_f}!dGo z8u2IH^zNAhg^&na(bdBwq&vL|-60?2c5w$De2<-lNq@n&_0O6d$iExR?p}RP5N%+3+a*~mQ&29-_ zX^OhB<89BGVF71BH9q}Ba)MdHkAN6-R2CQDdQB}(U23Z?u(s|+&Ek6SMGn8| zTT$qhoOOGJ{GRaU=$zkd;zkD` z)&Truu`$+19_(j}z2iBpw>_+V15a{%pQ{}g4p2wKIaMfFTC0Qtd>Me@WWn-i0e%3F z*>Fo^D`@1-g$ID%Sh$vrZ}nYPptVUMNv?l(|NgsBUS&k1Y(!ch;(W$K0e3i-m|zsx zo)@d}Sa@3G2rn#4j+W7OV@6-Im-PI}OH-{p6?`}mv@t_@Wc^nT=X11B<>X)afrig( zK)=#ft9oacJ6Zt5_*b3RdDrvG6VM0I^!_)OG_OSO@b2g2n7ju5J#s-IjR6W#u-HKE zOau>KfJ_42*5@T{w>kc70L@d%pIP@dk{O)D!`PSZhRVW8pq6++A8wdINb=*zO7+>a zq=LFc(TO~%Z%@yE@O^k49+l_$---+gTh5g;%JmC*Xz8Slg^dimu?}YhPAy($P$@7# zOfmthGyr$+}&DYwyB!IHFp0lO)W-UYOzyg zK{}c?qV5fFhY+#Y96-776EA3>>v0hDjbc_XHo&smEg_t(hUWv;gKkV@d!u4;BX>!% z0g|~b$sprZ*7ka~6a3tOzuDmXT2NQPW*@@Uw~S(yYkWWa-JbX=$Jz*5gL5jY`at8m z8m|RJy`5K8hE$qGR3J^KZ`oa^K_|%AkLn2vPl?srfC>AP{ilhIzO6qQ2CuHS1Zd zCpFleINzoNa}gtr1hC&if%2zh$On0s>HOX2nkqhwB=z<@|1~6-#5^OgWnzS71yqdv zzHKh&MqU1Qp{1o`Ho5&ib+--e5*#Ko+)?iBHZLot@G=$EdVBdcGFKTXb3$fp0R^iTE;MUeR z7uFV*sVS{IO5I~kJ-hPZaw{8Y=s39MlIm?{(m-xZy6VwigA3;k`7h4z5;w>ysN38C z(HRHaE$}nhfT72M5BmzlYcbHZp;}EykgsTC*e*AB{Bn_&$QaH3oEHD8x$XflpUY~y z%JZ{2V$|{VnZ;}D%QHqFt>z*X0A%%YU7!D5fDW|4(gZYz70b-wKH4H{Kp`hGevG~s z53sG2X%#h8b#h~+|IbE@K$kXLw1xl2XZ7^D9JZg77)PvFz!XdeiXdhIIOUr$gN7E) zC4E=d===)sUag(8Z>3shLq&o#1w2f9j$x3eG*_%nw-sUx?=R&DZ9X^xmI5e=q&QXN z!XG)Ayxa)vCVfT2=E}&UY_XQ);*(c{tI5do$b^A(mFoo2LSP`tDIj^@2)^DQfSvJI z9TjbqY)f?S_wo6>h8Tn1ieC+}Sk;zkUW!b{+bEmv`-sS(vGlQnZcqB)5#c*j#vG6L zvi0{iC)K{U5m_^ zllB9ahP{-xz$UiGbUu7*3xM=c^v5%y1|J~S20ZmCCK;I~oB4SFA%H3=A#jfX4)}RC zO)7+Hld^1I_j92Rxbesc!qL1^F(iUYH#D6SO&{8Tq8Vw2_hWGpe)fQw5UrIIoHl zgYMWbnCj+^O8uqz>|TiF*P@~@$9%hCUTBf-See&KPOXiP$<10=KIO3+yJ2Ff+=DlT zHXSsenePP`w(jUO=s_6t((m6m+%sbP%@q>!lg@KtB%G6@_3BTASz0>TbSzay38BCWfuX9!2_Wm;C7_G{30{Aee0!%fB(#h}&S9uIDWU z#Obze>|ml_Ti?X!=c~s5ooOK0x&bPU^p&m?;d5QT{U&N_oAQ|QaC4X-dY%gEU8CaT zXB6Y54r(Ny!XMpHlO-|mpXrRc=F*)O(-<1e zo~@!)Yi&Be_gC>bj6rCpiYcIfK0O^b0jPhf}CtzvnERefvM^|mhD=vMo=3MeGYtm$J znlHE(vrDngNs5?qQMp{GZXztb_tFdRr%avl9bX`}JFxH+# zY<#&f(IelRz2^fg?srp5z9+jMe$|Xf{75KyH*UyG@Jy4<5) zqYz>*af98n@x3LtHczR*KeC*?;`szlf{b}%5LN5L!q`FmP zq-iQi7^Cd_j!^dQsZ^)i@)v%YJJQjdj7gQ(1<#wR`3MP?u;ky*loQFlG$40+r_J@! zPqW~jb-H| zE>6lyl3JBz9!G5k%jQ3!9d@s2!enBD#YB1WG&7Dm>*&Etd(g#e7?}&+mj0m6+%C}T zzJT7YZT*j=R}KbYAXdx!uM$2IZYY!Tvf>>o*b1uY_rA>-dcu&`d+|YqSZ5IXjeFUr zcy3{N1K!J=k$ zY|(0G(=#9C{}{0mR_;IO?j^f0fU}dO?nn7S|7WKs8R14j2ira8Fqh;qBDv+K7M`S- zZuZ&TXZI}dxzIHwif9LU6ty9mO83^)4&+uf!xF;uFu<0-smx=XwhRXR2K&^>Y^5(1 zX@}L`^ml`uhLYZVSQ9F97vL-j+EU^Uwe^=nk&K^^eE>mY5sM{hnU#VGM zcT)T?K_Bxr;g5?A9c?~SZ|K-lc@PgDeWKm@w*$R%P#lKG3XDMQi=w+}7XK8y_^rZC z15{iA{<&!4VzcUH4$A(L4SoIeBb;RF=d@fi>xn0$U~rby7p?uRMgQu@S337|&2gmb z3yjISi@ywIS4qyzwvC)uH~4w5UWuB{*tYY_0+WjoQMY~nh0&GA9I>e*0bvO8PfL{S{RdxwLan`@ z+)5T=a@p@t!1I9>rA)pR1pJ^Y=u1LrGx#HUr?&i~daat>&9eoh%d(|i@0oawKJYV9 zu4-!e!yoWGl7DIJG^&;4Y*cwjV|ZUx9P9i5&F_-vK){fyzA3u`b#MrwR`od?wJJ^Dn=pRt(k=l7l`mam*z z@W0QM9x+#bIoKWVSE}G+vkks$La`L#C}6d6hNS1rJ9f4d8}A@w68=S37}%dMOgRY-6)Znk77O;_?I?I&>6_L#SiVwh_(~7AFk3q{ zq43$jb-E@KRva7gkD69;ednPjcKd~7->0k)w$oRLu`6bjWdp_XfvaKsC}<{7J2`x_ z@QMKbYxUjPR5JQ6k&Yuv86#o8dsxY)Nl)&Yc+XRy_Rrb|!1T|Op3Pr=6NxQuMvMlQ zQC*zJJTyh+W#q!oR+qXZT@DYLGlV>=+Lk;jUJ5lt?J{#Qds@He!Fc!9W9aEzwJXuC za7wfULDy69%zS3kVu&{#L*E4$hFnw;b>;Ri6f{`KM$i0|vIl5);(vi_Mzl?2_KW&z zVkyS(e0}L72bJo1asv~k*}dh%A$>yG=J-z)cMh>Bo;W_lx|5~Lh%Y7}j5*XV-x7j-X+yS4sis7ReB=3U zIG9e1mlimPC2HaEG zL>(+Klsn5lDJj}MSMYoWJg|^E`aD z;wx76%h^R}Xn}g$O0&4LQlZv+Ay#w{U;|2czQ%VuY$Jge3y!qz^>i0#-sQ5hkxdTu zZ8=jXFQZFxrvHhne`gyj+VdE{!qNkZH8++&d$U{Zg#Jv+n?EyVgZHFQ+}6sehER0O=-i#3J@V7)OFH zHcEQ7gsV@<_-zVZlOWBg=QdiVE=lVt1Hz=k6NI6vMDN~t;QQXT8s%BoTp5ApN z%?AJv{=MPO%UO87TtUK}P7}wm{y;!hd~p9K&r%T6qbEi`)c6fMCBt(VXlE`7TC<+7 z=hYXa6WmC=6u3^X@fsLodbfCA^6M=ZWvY8&$Aqdsi2-EAnmMSnN;kjB7t_#vW9bBw z@W!=~DpYli_8%}zpRvQTCp@N5k!ooptmOJ%u<#xv*0n>7JxYRBhC?KwK>dwQ)F<8< zhLgrQY=x)x>z&q5-oA4woljgD*qp5M{5JklIa+fA$4IeT=hD7sKv&Y#P9o#(XbFQ! zC73ck6mU#SpXHjrr|%#=R=SSZuU=5}`M(YLb#zL6_}ec-=&7M_b0IpKaSKfrgwIpk z{3`c3V(Xyq!1`t0*=0Wu*Y_)9e4#H}K#X{{z zBriMo##nsG=Gl{x(ZGFBA9xVr7vuZLo{Ag0-}n zWu}2qmxT3+H)L08Ne4(x{~w+M!2z5kn3&~w=Q8r&f{{O;iiA=)t$sbDLF4ftlUE3A z+LT2Wv+gC|U%S79Xt0wGn>fjX3+Kd3J{9jhIO1~ng6~kTP;eOF&=@PC|jPOagjIVr4IvcQUI967{Yjc8f*I{oX(EL#KjYJ&jOfF;_Cm z`TE~s`SjZcNAW$Bm1CiJ%DryJ6(BrIcu(N7x1R{zfWEAu&Rdg$HA$5JGc=#l!IJA{ z1(Bse3$3tSkDjH8IJW>S|y2k{53*ZJAN`45xLmwJ3N)FxDSRgJmK`%CzKF+vx@ z)S5CrDbV^=?g!D_zrYp(F9rt;7JXD4U&d7?TMGi7%X^cKL2!vF6Wc3<7yiPY5P((+!H0 z_}@sxf4TnjNN*?D7EcO;oc~+JdOFS5)Y!qmZ$JsUMe)7o(MilZxEr7C-owcDed!2& zu)}^^;Jv0-rr3*6o3i31r7MudKk548BYtpmq>L2=+;0N+-s_hxJ^wRhKne-NJf#yL z{44;|DLg`B{lum-b#HB}kLYT5y$`Ma?HFk^`bU!?;P%h%wLh9Hs3xc!$^Q1{&v_@9 z>r5R{G^kPfB5WI1@#j>*Q27^E;{-m;J7_Z2nD~Ge2xK@W$DFRfAym!2M~&zj^IBJj zWbKag_?IddWC+%^VTY8N$R|bxCXNJT?fgCy!9}xihc~M>p`9Jct?woR>)9sHfI`7@*SmiLF_+&5qXhaA4hj-UNOYwlxm;&^=rs&(T^M z_=x||fMRFDLS4ZXEM85{0!{XEW`pjSVTwLA3{;)Emu9A^84ij-^4;Uc*;!s7K{*BF zJ-~`m3t_V}9b*?%`>7VNx}5x=82~{5%lvOZE2piRdM;7~jn0--+=0S>ST@TEoF^z$jqrvyKeCgE1O_WX2Ub47RzI&`u~AyyN}O_ zx$H_fO*|zj%Oek%>jp+P{#Z-;q^*t3T%tK}&M5Ov`|(=X9Wx?D`S`=ZQMab697zfN z*w51uWtfT*CNDEKSHhaRao!2AextjC!+5z8RL;ZgZAlIctJ&q;$LW9^44|NKrJDhU zPS=N(OAif#Kdj5qLp!gXTyW)VZd@mWA4>ZT_fcQmKU13$nA(*Lu+1!{SB;G)Pwr<`j1_wX69C-2qN%)M3Nq~B`UwN0l0=1+ zApdNUuZS~y2ITE~XsI6k9Akb$?*V=;1|Vh8Q;a(YcwNqhn29JJXI3-V&&G!;(E4VL z-AQ#{H$!lPK4w-Q&5Z5}P=1yDdGyu7Kc$7Qsf8qJ?V)-h@oR`w?u?PR;%mO}!tSt0nMkXxqMRRinVDOi+ z3`fbY&`b6GE4_FF!7+1dQ% zi`4_KmC_-etk`k0yrpQyHhYdm3gVgG!1?Q(sjndvNG!}dpBFyoFunS5<0xlyw+&e; zJPj7278fPs2G!t~(X*1CCLAw)9_+P>h%N{OC}QE8+j`?rj?z=?0Jb}T{$X;(bno`6 z%=K$PD?NVAvA)bPCnTB@9uX?gB?Ve6f>fI>zpRW&7KIFsX{$R%gV3yYsv|v9RKl{!U9w=S#r~+_r zfMyX4pppR2)bAPe=A-h|_b-UmHdciRUjvb%hL46$zP-KYPG=n>UEk(-I|hK@0qegm z*9>BpvmtIsd)IZ8>2feQs{AIHf=0iY{V#m$a(TF@n$en#8+#4Hf}Yi2kNrQbeRW(^ zUDx&uEj@&Q^hii|Dba@P1nZr2^;{SqIqf20%Q$i zyo)D>r_0{4q3r#ua@j=^Oknv|R|Ue`*zzZOc!b1!X8A-Yk+r^V_;$D64IrmtYhzxo z@SGZC1q=qJMSvBi_3$)ak~jSkrv2Db9EY&g@`^8M?kyxzLtH8Nqnuk>40y3XX$Y~l z&VB7`ajS;wUG(I-8sJtPg9cL41u;}v*5>mV!6=xK?!DUaki7BJxi~s$#K~eUb9)^z z&vkC~?OSdMED#4{U2G8VM9U>9oo}y2JlPvDs{v*Ewd|xB9R#9HEEwRR&1{=*-hK%Z zxdd$+S4HZ_Ca17lbgfa@QGLbua&oZ>7rW~(r3*AoV+CG|FQ*;sge}ChZ z1zzA3DQa`}Y<9*fr-iAF;-&M|O@ge#yQlfbFPNYxVQypxgXyi?Izzo2F+z@jsmPZV zXZh<}i<3v20uQbND?V@J*fO*gnDw>B%wqJ=Ssjvj3&K>yA&_>dR1vx8>n zB{1N+R1s+gaW6NC8KXy+7O_kc!$?7)f{$~q8o#%@hl_kbp%m1A%JoK4qPAf?hs8Ku zZ($`|-=#ofpt+eku$TO|WGWvIH-5T?L3FP49hf3&YQo)2oG8E=A*s<;G!holp=wxR zmshN`%#$NH8Hnt`s0BmEE+xP z5G+OwE$fKOhshtVca%EX56wK6a0Y~(XnazzdJVkIPZ3lmp?Pua*GElf*QM3U;|L@beS5K1A6ME)spgq2&&(`hrU7 z%l0%aXPqn$fph!uxtZ>Wg$28NfK1cV>+(4w3u0z4!@dWlKJsq;l)veKN+^KAELKay zk6K?I{Qd%3SAl^Ax1g$9y4n=cxfq(B+P`~(TR$fd_Kmwur?b5;KW{}V1=^WR(R4~^J%<1k^1XE{ z;?7SW=_GPPg=c)ho>yB=Zec^pIN2fCDpy}wxTB*w2V6tfCq-HF0tfHb*MZG0AZCBF zwQhT^OZtVRpK@{zeXuz`JR@Sy^`C+{xqgtkCWwjTeyA<4<9NZXY(K)m!`Z%V;Qg-@-cjrb)pXtfeRkaY(;-z| z1Rj(73K=A&ahcDf77o9P&FYkretwq&1G@(r-9%}yU~E#?TS|fN(XhBb6y6#7Eg5tJ zLbg8EgT5T@vs{zTyzU9sd|yEMk84H6u|4gtA8eW*-ZfS&e{1c08xlG7JbE&k1;}fh zhRIJ)uE*f)K3LnGs^TDumphWmLl@ru^nTuAAi^~htSApqzeeK8XOZ@g0}pr|?4tHW zNKvCfy?8CDGKSqv-T7sNx{Or~zUmu%>6M(e7GdV4l14(91}=z_8#9V!GEMZlh^}t*7n6XWZ5KYlASfeV3sx%HIp`cZ*k~ zk%t)>16CEm^>h%$IRUMVm$Sqnm&unjf0d%)eQjFF^lOL`RaWhPO%OET;n5(|_&hGV zDk6d5pEHb9o~cim$V7a_!jaZm3%{I31teg4#iw#DZ!=RJxiryVRUyp(YZAFNTH~Y> zHfWFO<-6GnubK9LeOWMYxl;~Ii|>&5_p;#iY=7L7j8haq(8!UgNDy7_!FNu+h*O^A zduv(uSdy!UZ+aLPJTShzCp#(NPJbsowk9?%?e@?}2TPY6OHG)?kY~q)T!y8&DEJ0*iVzg_pkhKGWOw?FnvsE6~|jR z%Ut$oji;9WY;m82+eiQ%5*th%BFV$D6!3D&$+!xLXWhHC=zNxUu9vrwF4vxk7u$6f&oXz|jovLdwrO!c;8fxI%ib;bEMGJ3op z|DHl}`AQ>a(P@rNYpRj}?^i~8pKFK@$V!k1E8<^EL>Vi;!PU~tuhzgTTRinYC$g;T zuTj~J6d)bn9w-bYYUypGKiP}k7I6&{XBkk9xPe<&_?*yw0k3m##y#VkPFj->q7Cg}iLB>1dDNq&PkwpaT>Qm*)-Qpfa?~)0r+dlh164 zDL7z9iv<*%zb`&u;tZA`K2arG1lScKm4!oupY|*ai;+aTAHP%Qw)J(1+SA?34UsfQ zco~YM)4f*BhH>cEJ=$_Jd?)?y8$c%hQy0a6AX;k?VWY#?B02qd5U?MJbUNc|Ws3z+ zNev_8%a=r|t)17F0vx6iqd_(&3E{FEf^Qh5<1hA;c2GW=_f~~FxnQGm9M3#HXVN^7 zM!^}w-5WqxUAD`{uf!iEAai9LFy#IPbV=ZGp8yE|8hrg#hIEni^Wrz3W?{+&i{t%X z7r=(zP)VD2vzRR6jz`Si z#Jt4DHMErJ#pbtV%U|N6?hCK|pkP@rxr!l>bdeD1on!@v&#G}UC@RJeRX>Hv-Bn-z z@^>9?)c~$+Z~~Q*u82|L!ruXe%>QL+j<>p!F^w%4e&$GO0#x4?vzzXrq-1}!GY$RsSV!FJ5Y zaZ4WhOK-g&&N zm3trz$VxE(p@+VM|I0;AAN<_lw)wxQWtf&fIGq17hHm|7`%k~cv(;35ER=YYKW-*QxuLz*zId0f>*6^;gqxnl!oF~wukNsgTn=Ew!aYw2gveq zURS!Kl8IJ{|JIuA<41P=+3%?v?*RUf3m!<@Vr7_ISb3IlcD3m$;-fY*x~%*!PCz$m zfY8Q6Kha+9$*6mAhd6Cse_s??k)JIk1|Fu+3QOBY@=}+YC1*{1XKI^@%kMs#LpM@E z_Rz|H;pY}P>9pkY#a%wyZEyQqSF)dGL3>}Q`qSiyWoJ>K60kcPX!q$hqHDAEKD>$7 z;GD?+&+7^5)8sN`e+31p$cVH7~k>+QtQ-X6^Vm50Z3$6B52+%`LY6x~W7qg|2UburmSE zHgZb61^L61P{eeA<9uDzkia}y7SnW$M@$%qb*P`)r^Lq_Vh^j*+yq}%p^#2iQq)In ztVsADoa0ir0K8$+g#2Imvr;y&Ebtwd$s_Z%2UEtbjr)merILQ?IDumeRxJQl)OIXLoP$l z)!#uNx1FJP{rt@~svt5k_gD5pF56m-xfLd)7WC+#m7`>>B%5Fx`8W^TZ|RAM#`Blj z{F6$98fE+}*qL%r&$C}z%&4xhJ2&-t2`Jd|1LE6Zp1ARFb_!YV4&8TMKdg%a1kR1r8n{Ywa!{Qas5`b8gB;cn~G#WX}gw?kK+ZQ_}Yv7K#kPhfbh=~1_b*M)3sgGKBRL{TC7&<3fP#;)79PR z$`v-N(;5*Xkb``j{bR@V3IEL)H1V3x7AH^@#k;6Jui#iy<>=r2PhutTyI7)jrI+)~ z7UNrei~mifWZpv2r3-|vI)Cf{(@-me0A-2nYoYPIzlp%#Fa@ey15mVdw?^RHaZI)h zV8Dw7fCzhOaT%@NZL|)+BHG@>$N%^oNM|z@=DvB^3?L?r2(Vs zzcUMacj(J=`#O()!?CG9^veU#uX%0N2qXF3=ka;}A}o;wVKXX~Vt!V3e`Mvze8qZ4 zqCdTBw2=C~%>QXxiUqmCnq&v){uZcT3xUt==su2|Ht{>@M$*^|&1j#c$W{B!&c zsO7}`M;`ku9_fM1(jt<;#~;V?6a4alV2OEQnwS!CkiChIN62S22sN9zmdcv;oZl}( zvvB9prL+Y9Px6c6%$FwqPw8Cx@-MhZjxDV;Bod$Eb8B?i4vUlvBsqfto}G4`QL7zv zrJ-SlNG#JZeVZ1fYBfu$ydMj4Nx-PM&CfYVCOJu9>dLtgAKwzHneC$f&coQqNIP`v z?F=F1TVZF7k;D+Dw>NzFdt<^hxs8B1gydZi2y%6j5OmD_@9=3yhN*er_my+2n=5lS zs5*97HWwUdoF8{x2<*X4UTBU;7rSZ-$lzmuj_yX8U2A>6Y#b>wGWpr)13Wz)+ySeMo36?Yp-sj zR*l1gyJ36E&--3|+m;wL!MWY_qL`s93^h3X_@Tz&0RsfmbPMx-fJR3rrd2|6$1)Xx zR9p?5g&_V}D*DTYSE373P@O%y?@gT3rgC67Mt-%syJM)N#(%0|%1^Pdpugl>n|#c8`;j0pnLiDs=5;#{_1P(W9GO#8>i6T#uCr* zpbXLPP1`O9hPG^lNrKgHEw-1d`Nij%%{6Zikr2l=(LsWm2Jmn5Kws!4gvU|vmK8?M z24J3?s{<8yVk%eN(p2i6@E8#mmNx|~`R!u4$fu9}I*Z$E%O^iQD2E?(?mm@G><6yo zz4NHzK9UlXdTsM_;Ts!UjN)1fTzdxH^FU__*mEv&huemdeB*XE3;sl5|8o+YI6MrZeRv$&!aGc@hGf=7s3vvi<4L!Htg~VVXY;h}*f8Ya~@Sr7QjjiVV5kBM8 zXzsh?v7<9*%=%w;kCPMMEXIZM%cx7GTQRqazrYELc$cW@rmxPdCjwbtkgYnkrPQw) z?7_nw<|n^@jl1JL!)(gKRLcFb6#>Sat#$?5Jv(I6J~I0pSJb*%|2fgR`GrPPISuPz z-@W^o;t_Z$Clz6Ijm=Sxr&yo4mKTw#>XZFf-;hjYrov zLlm69#*=M1w5D#n$^tLwaeVz|xqcW6x~el_=h#Z)`e&3F-LgiA!|x?MdVVGo<0loy z$#JwY^I~;tK~|=e5FT`4&>nt4zAzyClpWn4r-vn&zFPXe0?)ahV)(H)LUqV~`#V`# zaQ*!5MN@$3(s8y)<+V9ZhE3a^-HQ#SN7t=KaXGk1b({6%Mn_08X>}!ARcBMOf(O`i z>-FS}E-Hj7O1_TG``UU&APk&Ic20sY8)FNes(p_|`&y*Euv|o=SKpSe$I^5h7Fkj( z>tT=n){;G?c{nD^n@k8JcA&Aj%6YWuIU*f7P0ww|QA~fHP`*|dLg=07jBr-cXz+_+ zXw9DJeFNUV)PWgXX=l zDb|;G^3xhsISu`xXW^lFWcQ6VHG6U_(FmlY4lkBe!LXOTIc3m!9$!O^@!^hy>vB!U zsRtp&=XEP6eTg=GSSj**-6wsU9^}(sbw8TOw6`u!48l3lgK0EB^JvJ#dBKIxwRU|I zP)<1c%b1DgT{O%6Ee3NL6R30C_>x=pmDH(Cf0_($d0PYIn0hA|X9FLmc%)o&PL2=k+Z}vcc;l(% zPKDDD{XUU*Pc4?RI?P1It%7XBIXP^8($6$2PLvFuTHez9lJpr#q_sfoL-y5rK(QKs zkSIwl^X2M6SAcF71)Ay=di54I<-X0t?1DwM6=qRNWcxEi4(E4zgX^GEB@Zsu||o7ULkx zDq<*=I%C{-lm6joLI1*KZ5yrS_(M!{w_|O!!$>}VS;RAkhL@0{8TRERI=HrKvdd0F z$hfjB1~#NnxUViLyKT^5=2@G8^rK`tHe-deib|hlPi_@}1T16dOdI(dUC2d`OGCas zl?~vBiUyO{A|)Gpo1Pw%XF0FL9Q}&Q9C` z_7@FQXB#h*oYaz;u|nI#mf-3BW`S$$C)37pm+5p(7z(D{S;nZ`#!hOsVD-zN)FU-P zDUwZ35T6-L_vpIY4VGL%v~(&S3!srOXc+utJYULmb9AK z+7bghN%fEMK9i#+A6?oEDymo4nCOd2=;YWsJLYUCikHlqR5d>6ER%$PsyLo~cM#MV zM}GFI4dr&wr7Mul>e-7x1FtF8JKf&zoHud2Yni7vpi*`{PKHG&9YzBL$mzf0F^I-2*0C zyv|R-+qCT4VJcpm6L&<)y|wm-0VT)fSK6{%!;YvGRb5)XsX|qZP-}0_`6q|fvqd&c zY*q5Wp!df(_f)W3W>}WvoyW|C+aIky0HNG|>>U>;@&|6P?#M8fmW&ay=*$h&0ccGh z9(D;DDRd{EM62{;;-E2HX^U@;#t!hs#I5~ z%R-p{0>T9V8&C@fF%5&JjTLB>UD1qMdop*b;}X`3bD>~8bO}XoII@|9Ux3G2#=yqW zg)u{K*o`=t`O5=*v4&)zU zm+^VM_{}kA%GE7piZ73rt)a~-h*FWo?RFDozyQA9yQfvQhr=8EG^Uks%K1Nt0t>L4 z-pr!LZ}w84CE9o6k{F?+q5nVj*7Q;1VI2YtlrRw}1(G=%H~Cq?;%8e=qq^+~8rASm z$4?^JNs+AZf)c}bS-7_PUT}WH&Ql$xvTN6~d;elPIq$)F1-O^zy&O{=7d#+7{0^6f zJbRYC*mFaGM5Eq=5vEnN2!R4?VPNB;(1gM3@*sG2Zx`xHj^cxOA>~2ha1r$jD@!?< zL}o^<=0|8zn+yiJw_)m*7H6PPDslX{CoLCI=JH8`LjYcI|FLK6n>8oYaV^u!<4MOo z*Z#3E>c21^{>uI(loSX^35BYl$q_x*cInZF5gz`lL0Sy5~I{%)B zwC6MT%TmXOg30my`&M@A`*pGrkON)W;DC_vvm?r=%H^urNR?^M=W^!r6^?Vna zC%gW79t;ynNl>p|T_~a+anD+TRnq+!WXr6lWz~6|L%a^7- z`^~l1hrpt*4XB|rg!!dHIOHgUz7;ebyp4Np&QP;-vc!FKqPJ4A+-uo2eniQ;<>hLY z9X(dq%BZ6V-uj3_=Q}*y%ozSVNl+6W(~$t$@B>(xpMctJO3GOy8N(ML-SfQHMNA&m z=EZzp7(tQoK9Q?h#=sE~wW>R>w8W~Hxt0(%i%02JwFAD&URTvft7C~ zV&0b*g+;RK`xBfrgJOlFjyyLx%|_AT z;h-{kb@0PN>p%ha_t*ZT)S^g1CI4^#rd+1WgxlTdh@j_aGB8np**m)EP;Vg;MGU|3 z?kH8XE|5W|)7}k~sn%P@LJMd{v=XvK9ve2(9nw(@T{uB&XPI=-q~#|@lbZRB12v?R zwb5kM0c2Yr*KZcK@alP1TmU!ljIG|ySA8+8m}`%$GnS)JN$5I#u>IZ|r{~=c??IRC zn)(NgCCi8C7V`qvH{AtfU^XL~XY6O^wIQCQ9?|4 z&Uy$-;2{;|lxk-_w#>(h5Y?7dO3dtDHRB0B$S48i*c-+bd@cOyw)<{K2>7ZfK0JDd zcalzcHL~zTqBpvl{#?x&0oQNH%o~0w5lWXdL8@U$^!eBm$c?3@19n0S=na$3!+}j0 zv%Zb`C6h|{SUt@zZ}ME{BVVWCHvQH|;#Wofqx%jf&!CV+^cb(Z{Dh{Wc-dZl48v_G z*R{e4jauPNVdQ+Elz)bWs_iBzWM>a+T9}kGcoVD6Yj(Hy_SnrWMy9-g=FupgTZKo; zVVf>eUZRdR){0jHYhhP!RWB3F(ATSJWG#~}6I@=$l}!mP*57H2zoF}J$)@?QA& z^kT~O^Dm9D%6K?(WgAd4a6M=TO3!K2-DbKf!w0QfYC!uTz!)^a)E9!eev1kT9w2Yc zbr=()_)=}MTml?W+FSwQQ!eoms&udV^EZBad#XZ2=+bWN5<11}r%lnVbmLNpJ47!< zO~^R0=3TC9>DuO4UTxME6>e_B?_}iw_3uAk6J-VRSV&R8LLdPRk4SrnwUT!0V(}u( z>2Btj?q+90__#=Ih&8G%h=P09DcHXB%)<|}JZ?$3S_WJDvXQM6o*Vg$tVRvLY#dlZ z1JrZz{n)0Aa0!aF5T7qBY^qh?CS(T}X3I;!s4v(XT%()cE=JQoyeIz3nH36TMU-kp zU%U4;cD66ar&^wuNlk8$5H7b*zH?|p%gvewaC!Dd+A-kxV!l!n>Uh;&JD651wMV7{!30+jiaK-n8HXU$Cq8OZHRd=J!VMPUHfm3C|+ZR{Rrn9!SE0 zyXiA}*2nDPAdPwGqp=&5z(mV-SWh)fbo{>=}n!8 zJ99FIO6q0su9%$Ly~G%ct;(|f6PivfAqy8tt_HWDs{0xy7nbrp2sQf*i#U#hC)HK= zO}ouL?6OMMd%S^Oc7F2iJ(Y}>d{ZEw7#Tz#Z7S1(2(+_cg%*Q#ka}B+V0PRw@rMf$ ztT0bW)JO3SG8Z0#j?hZC1UFt>>rW@n-YmLVy8N5>(5;_VR30gR0J3CXP!gXX!XEk@ zHO>3DUbpqdDcTH6ffOlJhlD(!dgx%j5)6V2c}p5`u2*BUaJWHK8tY_zX{aB%|8cek zr%xbIyK4F|Mv1ufyfQQepYNWQUY z$i)qQb#9p)b8=lquwJ(9^+-iOeTn0k7+y;vW8X9y2iJju-dsvI#}nB{xy7f!2hkxp zrO~1;itYP3R^T%bU161ELyn9Ll;}^yr=zH_H zR66nG_)P{fAX=BO6t;$xMlOA3%1+B}Dp~gP#E4lpxCx1x(sRQ(8m710ci{O_X7>OT-Z1<4=UZN7#+1wqnY(z<55Ht8tU5MjgS%3i5H(Y!xhbQ0IG(IO0`AZxFV(g=R}9!C+dR}ZGA_Gg400B5n=ly zG~xbr=V literal 52029 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

    ZpqsL>g~jyEr2foAl7wKMhJ> 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

    adtb&wgDsV+dIbjD6K_+#3N_c2ic{s8|&aAGXEq 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 bo^b0mkl8+`>;aw#2=h@yMz~N==iC1Qmdcbq diff --git a/images/tool_curl_import_action.png b/images/tool_curl_import_action.png index 3fdeefc8c92f5f899db7f73af50cbae27512baa8..644cfd6f691e4b916d04aa9c5bded0330dac0043 100644 GIT binary patch literal 52909 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

    U?3xMw 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 UkqL1`J}6UGQcz>% literal 54704 zcmeFZRa}&RyDtnPptOp#AfkYDH;jU`fOK~w-8q2L9a2)#-AH#!jda)0&CoHxdn5jz z^{lm?qy5?M**?(GnSpzLbzR@OC-9TBC?*;a8Ug|WrnuM#IRpeG9|VL4zaOCh&-73> zSppA_t;JOB5D>6B?*1Ug&|(oIAiP8n|L|VHIcaCk$r*R-vIVx^Z&^h(WsmbIL!|{p zLVhLx5e*0KL)_K$fFJmXF{!yaLU~Z(tjCXDp@}^c#@WORdi6-+3A)z9$aiRue~p`8 zU6*B=m)F1j^6n6vLKVI@*w4M6#NE!ipTf#2F@jfw^oZOCM-ccc*I`uth1Z{d;s^%F z3q2M5=Y_1-G9Kc+`EQ3{`Xa~JlF&t%+GD)!KeYoN9W z3=KcX?TiK%XHK*ahTu~gbNPm_OHPejMggsJ^28>!_I{`NwKjC32L~t9&oypKIN&+^ z6b-z)76@AtCEecwt;#kF*!vT&Nu|Q#w3R9^CF(cY(4DO=ahon&>4VoHOLZI*4}Sju zdsJD>dr9ZeYB`;(rw*43vkuG2PgS`fA(458FiyA+3gdHxOyWiMqH?Y8pnOcmosH{6 zU$gDDc!9uGV5tfG@Vk-}{&V+Dz3~!t@rC)u8kpbq1$?B_{M?qELfeBdR75_@A>*gy zkigu?CPOT^A%anfF~@3OtFugNxaJ8Nm?`>XTZzT-*t*~78G=zo!dM?@H65KXU=ro; zlC#SHd@87@SbOlr?{1ts7Z=7KMJPscJ{q*9cWNv&A(1hvc5N)Re^Pv2G1VXcq~u_F z+se9Ks=Dmj|3wJV4GY3;9SpAt?#dsX(mT_y zHkhxDk5E51Sdh_m(0zjxW5#-aHaZeABpxrR-+F{vByt|ibuqdup9~dhT_Kb8C-`@7 zuP$Jpt&SM>+0<&#UGLcG>C6pxrNcx!02_3hrjZ5Z+N;#!F&8SCu?F%Qwhfk0_O>5vEkIn(VaDZd3sq zyTpW7ot;4~nzWjk^@#tTv_XxrI{#9-REX%Z2>#J@k1|*N`>#;b`22LJWBmohi)wRf z8SHwt^~)QCeYkcmPQrJtTx&Z#EGI5jT7HsPBGkl~xZ%Q=5^xd|8z2A9$VjP2$SRiZ z8sm{PVUd{E26kVd(oKw1qBS)IQYu8bVP{d>bLjHBl5XGOg}ZY@3KRlPCEJCW{)o}! zRI5Kfh?${EUGNvlAa6BWJ&2?@zrM-6qWxRsRP(>#h(v++Y8$v#2vZ1-F=jkxy`}Nn zl|L&XM8rpUjch`H-MZn0iU>`(@i6b53~4MB3yGCKw0_MREJR2+3s#OZo4{AoqI~=I z7x@BvEtKWvk`}3C0^Lg~BdzrN)r%r{NJx8TR#tkxnN9lG4={gugOTJIemMC@Z4mNk>#S2bgNZ!N3E0)buuf}3sa7oOTDt=covgB9~ zbLHy{^dH$wzkyv%R$1I4dUanOcj=Wy00RkeUVRf49ZjRDZK~e9fQ7hm@y-c2p!DCh zJI`(OnF%lqj0|g<~mJA8a-cAY8@t1V~3+Lso~ni`oC2iR(%wtJiI zoT=`0m1-6mgfSLz<>1*eU}%yhsBStOiM2q<0(R{$A%p44Cd!p1-oN$X`;yOVpL)mJ zj-A@0_=7H$HID<*e37iu(ug`!)14br?2=~Y6lHC2ibFx{$VrMc2I-+5jIHmDWE`yI zbBSSFV#%^&y$d|>cDcNdLASF!Jb`DE{-%0A+&05(Xp44W*Ky?b*KNkY&xz8@qb$Cw ztY zzkA~U518;T2(tB!(uXz}jM28%N~ci;x^1j3>fVV9hzZ!&JVw;2V#- zD}LBjqIlY7vQzxdcl#ruUY0heT?d3}I0k=P1@0=>0a47FcpKu`CE?01UMl^qmn3>E zZsXj<{1q>L=3(dP>%GP)g(6$&7^-^pb4Mk#nf2_0g&ZjJ2JKB*Ce3>1FFt?i7?q0N z%Jrfhjfmb}ad9>)l4p*MTPe|o)ddp7BHQ5;9Agzu;%Yq$@@MK^hqnS)yD$2V8ubfs ztMB&NpgFq$IbEq@63$I@T_tC!btnaNJ>M1|rKZjpuldD6O2EOx=VZ~N4`ZBC!q>k- zNGdlXJ8AbPoolV=;!?=Izl6`}yOqz{hNduZ5^ll-8LF}QBr@#j`7NWL^aR1L94US& zVHT0~>W#CYGJ=S}_CsnAt?c|;nMcG~pH;aiBQ?7{9h%F0Nxkg0%qHt!V>TAAw5`mjzO8H$ROUmrKlqB* zl$t1!aeUC+!Kl8k2j^3Vtrv3=UK}PHR1J#jwlubC_f$iq(ljOcWgM%SJnaQ*Ar4WB z;A^g!n>jjjS4Lkc*62+Sr(Y%eoIi&vE_Yu$GKd+r#`{23=qO{XjIw+>j7DiIEwYU_ zC-zEHflqZ|pz#VsFJZXaVKB{xStVRakS^8AS6}RRrY)tj{zW0SDd9uKG6j(nysyP+ zKL=MP?Zv4H-I2(fbm~a`+*Bey((rkGqxKM7@Hx|@U$ut5&UFZ`W53L5=zEyS=mt!Q zqpSSY$uqCxib{9uh7iT*=#9~FmDTw67a7b*Q0KH!6Y&{5*QS(|{?1Ki_v)E;D(yP! zIwbe9v4MTm#-xRJ&#ID4nEkf<;ZXdD zUzetN)zk|YNE<=fZ%k?~%L#x3w^V;7PcYo~^pHYK6@8*+V|ozB1^LfHQv(YhAi!M@*27!Wg&o$_bmnX*Jt7EZMMq52MsYnVkCqaTt;h-)pWO z9b|U%I+>ZHTiAeRE4el_A&^>miEI+-g`!yv{5Um#E*7&cvp%vFj9G_ORp|m)ycWL;r*IRFF6+=ss4@TS5uO%jZp?o>CUu4Wn$Zu zbph8a-}@ZmG0{pqKfP<>cQ!X>w6oqHcq9L^;HllF{^2eIc0SQ!5W4yo+Wq%F-eeOc|MV?KIjqWH?+(%y5B>HS%F`GWf0B6hDGi0&pL zoXD~Dzes5D!M1R(E+}og(NHtIlrU6K$bo3zFyiEDB>HW8#+bH?-AZ?;;+lscOmL1I z4Uay+B85d7D3*08H9&wxi~L{~JluBDEyX+A(A@uv{@T{re3aSJO8Le}??qEZ-@$KkXxE z^MtKt7LnBbZMZ=4$K!3iwQvlw4C8682w8B)VS}q$6qMb3N|WHUqMLYe*iWqsJ8XTn z?mIM7MX`mMMSUsI9wuRjbr`6^aqPodSUnSJN{7QD^Ek zV&xMhEPN|rW<2pKvsb4;-|Vnv8u2d~-DyM;b=R1ngKwBt#`lpvYX-FdCv?{KC!$A4 zVj&+0bi4F+UI{JWAk6{y`ih9NSRR^2smT1(7_s!Ryot9vTJdU@i7AY0O8HWfv|FBvrb5&gPOql& zS=>In5~3m%!p~q?+H|fHaQo(cmM~S<-42f#l-RdZ987#QG_AKB-U`Oc0~Jpy98!ol zYLS!CISps$g<%VzdwLcOEE%tIGVoNhv*@Zrd{k=u#}IX=_$W7UY0}VHj>vEPln9bdw#*l*w9IPf z#|2SCnxSaRsbN&S5J<4RxolsaM2KkErMQ%WtRk{!g7$;-pUP9O>sUPOPO2@#NKO<@ z6JIHA(hd!-nmFJ|Mbq_>d5udQEAs3KUzgBWs7%M;cU#jfXeVjBQCGaH#atC_E51~> zt?!4XAEcjk=eViGpX7b*UtUjL5QVXAh~X7FGM@%=Q8JXq5c~dtV*|DlBSo=sT{sA& zn+9zLkECxCZv^>7_go-aNEB2?x&|vQ6(w&PDjQawis(9TNjX?( zZSm{{XZ3Iel$i5GP601OJkw>F;Ft&5N!NNMbK}*npOGu@zfd$MtsG-mosOXFDXUj! zE)@HnH^sX`*|#afS+Tue+d4lfC1$&RMDe8uA&X+B)GP=B7loq`b@4wJHTx2j-jdtB z9A7{DD|$H$wwO#wl=YeRxsaj)u2m(0=>C-T$@aQZKL!3;VFelQwTas4CwrsM?$&LaUjefwK3Qg`mS%RC~-TZlS|=git=1Ox$ev!&bh6qQx05(;bG zS;d<5MI4kEKM?if)WgUU)nuayA;jrxnK+s%uaEHeUxl?%l$pD85+<^??5wuRegPnV zlBTHSzVhK0>{C^}^`G9b*%Jln4gxHt<@D-wXU)BL9Rz**k}KfD-=!K_o5X$D3BO1N z&R8w0LdO#4MrDfJDT(*uaa9jvf{DLxa>WyBjIXp2>@_erm#A&}d&3=D`f8@CY*(Ed zyaSkHU&PQHKXkS?AkDb!9$L3#CHAB6v}2~aZyF4)7mIbqj5be;$DMXT z-{$>U&V+B-^lHjnHV+zZrjHAc&M;{8J3(Ivy(3Bk^uNSl$ zYvo5$s>@A6qZJqG+QBzedgoNhrZB;=@L#_eL=_eiCU2!h^B&WD&Y>L1R_1W2%9m-y z1FqAN2ap*e1m)9Xh0?XRq~wM45}>C7v&J@C$+`Nkx|K$86KcT>+uvRC+wbU2hqxn= zK)LW=cHIG5`X>Dk$9!a?r)b!l8-jy&PmMp&zj>T?PrU_4<)Z%UX$KKL!^(>{JOa+_ z-h(n^hG+Y&E~USeVq&7BU9PVm2xvUH{IXwEAz#{sVyBr(hGpogS~=Lcf_w@+OXRmM z2uyJz;HFh=PZf3lc-t|U&i-b9t?s~_9wLA%ITHLu>UgY5P^9TIun6IAg$gS_=V23X zZ=9jU-;`yXAN`&$qi#9aM5fB;zSo~{pgvpxpQNuxPiXFK-MFy6_fy+E^|9H{mW)nB5a zvJ<5|n5HB4C3T_tem`f>W1FszgfEU#Lb-z5D3bdtEsEFu)?8B(j9YDruz1~*_&_w42vZxmcr*ql&Tci>uzzGHht`GxCCygt>;4bw$uvX0bmG=+2imkky3JdC1~##nY`l}J!j0?} z>}ml#xE5OZG($1J0R)hrn1z~+S%6Yn{1tPc2Hf<|+AJY!!==kl?tXcYlqEu884crE zsNv2PG9Rx%>a9Jy%@DUD+2ZU~PWXfR)N$nv!6qw*gs}E0FxL8cEx@E6e=X|lbG+VL z%Cos(%gtDAa8<1LFYWm<2A~<%9}4`b|0prh(8`aG+FkNw=&o2B`nC3;Gs(X0aNpnw zl&-WqD5-M;Vm5eXu~U?;(lnonTzd7g;22}iRQK=|u{XjUWI_^hRsE>M{YhL2azf|o`rSD!fzJ!g+(EBI-QvY zmjDev{Bls@9$?Tb>PTVPbhrYg^h|+`|6BR{$*i3;QG4vUy`Ao zxFGf4o%!?IYoEV^l$HFqrxij!KC-gX*I8M%xf7;*MKA@fH`*$`LfFmbxloGMI>v2S zdKJFu)KYytR{!&nVKTh>PuqY>W-_Bec9{I@htKQhSyP&=)uz;FeD?l zP`w_4xwbEa8TaDQUe_ham~jJ)aH{?_{f_p2y_9GD-iWVO4$}=G?dHkNypWY-^=cqZ zMbEwB^;8^2^DLz-ionxWIyoJzZeX&nlCuB8O>XPypFyN|H94lY;5C%)d6pzdTZHmm z8wu2(y4FD32q;0uookOuPkGPYhOyL%impAsz3q=HGM`)49d^HZ8?v!t{YSPMH`O$X zJ4L}Owut~p-Pq%>pOtqP`nRf;f~HMK^Jn@##3V6O_$Zc{HwMFw%n6Rs!hs?gV~Pwg zlz@>Ji@<*DSKV8@!D@X{b*I7{m^}b=xpFhxr^(Yqh`B&R0}kjMFTsVa8jJdRCexx8 zIszck{VvW*0R+2v5D%hP#kQIAtuwde!pHO}n5nNKeGW}FM%kh5Lex+OswF$@!sj(0 zEy6EtXs+v3Cy%wz5{)SRL#Rc9#`NSajFiwqhx=}&e^YDrQ7u2cFqNgDr2-maUX%0f zt1(4c65(EHGK+7;hD-4u9O=ajS2JG&yt0HTtlUDis(*`Q{gp_g;Ea(gP@5>p8EU{m z?9N-WqY{v<>gdVZmodotPX(uo^BwR`W`@E1i-w+l;g0k|qTTn^fa{5wfuPC?P&xsM zy6@;F*pyn+GWdftEN$X#KZ%jygcc(p>_L5hA!7iN;!i32&TS6q3EP<#lFLw@uI0Ip zz}e3-J);{pcRcFSAf8JZ5H9d`(w5Mffsm@5^xqZBdGKJ+RIWl^QvT)fXM8|*%OEMq zwy@*x<9sN}uc-K5_J>fjc?zL+OY|IlWrG4fyHa91%ZW2X6>9=$tPIUXkW;B&LxNVzed{(J@VUY_$-mPvZolOp;8x1`|N z`L33UnYl@ln+RKa${5(!B8)_%43}`aD)`FM!29uMa>k~Abc?-x8M`~=;e(T2$Vx+= zbn3pr@4&m3Bci!ru5fViV3lEI&KJ@4=vu?B7OUd@(g`+NPc`bDv^mvBGV4}R<~&zc zVLqAMYWPRSpjj{|VwiYlEJtvg_M)D(C5lngVEw|>Ca_pyRbi}--m+gLh7!M12ap;Y z*!)(BH9Pwj)Xmo8`CBe`En}@PD8rw3*6s0fGB|CFehA0kBTVW`|0$ichY&o$9mqJHNj?T7Zu2pDaMR8t2fH1Jw9BE`kN%0)8@#?@Hupo{MsMo^d&uTIsZ!0d893 z|B_N>@1_7~Pc`L{VHI-H2hVKpk4%x1+Pn~#r~LzEb~H-r@bF*yo!N`I%Bf2(g)?%t z3#q?;e2_v5Cq2p-qK*$mb2Ju@${2enY7MvqSVorzF}?WMBrqGzil`68_GT-0!U1k? zeqv@60a+5Ms#>x^48NyBQdbbMzvpzt)eSUZqwPM!Nh{s?-&|aU)lbcxhKI3uN z>jz2lqC#u3o|&)OdH%($8LH{j(iN3{W`}}T4OA9Xpmb(E8#<~d+a^7|C!(S3QAflk z1XGM*O%#Wdb=_a3(P!2~q&tGQ^0!#LLd+RW1JfQaD~8&`T{=Vy6KFbRS4!wSe}>z| zId(<(R93z&2x?!=*$?1jqv5ko49pWM(wO2uvKk{C1e$M`z?1F{;t~hA*Lw8o*CX-TOwjrx8FDw*sY{qk~*BxB(O3+hHCM0E5nJ6RILiK-dv4 zxO5OHH*_M`(zd3_zh5`1UQ=#C=U0u5zrCM&i9=Mx1LP;2diwh04}-P2VgHGqJrCQV zS6hY_(@CFlVw<=^R#}Z3Q%2!u9uw;Ry9s{Xy0ykYxDQ3Ky|u;mG}hW8)v-%$yN0i z(M`{RNMV7=LOcD)2NS$F-j?=uTSCAsK-&8m5xKiuCfg0La(z!fMm-W#7I$)ssiV%Q zs$*xQF`hw-#O311 z%LehM4Kc!>lL{}Z3_h@uOB?o~zOCF!5O&l$$@WxI{x^uTn_8s)XSXGBpE8IeE%v6Q zpp6$I0{|yM2?;-yxZT3yc$S%IUBO>|`Is;T|4>!&`Cv&&>ppf;)I(0z35cO4>n+Un zPeb}J@aFLnVPSU>CCaR^Q9m6o;S`IWefgY2qqlB&7t09+s&5LbRfqij?k+dz&AnfG z!O@qB2`p+EsVw(B-v{Rv%fGp-`H_#8b}cYM-1+u=Vw9;=4fmtip#5WR$~vZhTt0<{ z`8=AEsvyS@nH3mzPJZ{nN6?xl=gl(@@E?D7)X4awV2y(#8n6yRO{|2tz(AMVD*8ag z-Fq~(?6yE*sDenf{wRSXUw>*B%Y%-Md4`AAw@k(doHG&Ap%`8{NmHD>Jl z|Cv|n{Sfg-1^ED0(@{|L9o)sJ;rqQrB!sM zUD`seI0ws+N2dmZOdXQ*F6~)tWYm#v&$^h4>p;Qe&yEPKOWwSW49?tP-m$Ar0H_A5 ztzRhdrNNzIV(i*wyFZX!x}A!~s`c>)!0q0>dtXA^4BsOTskBIAZ1(EywqnlvNN8p_ zFF~aYiXWJ!x6E$V5ypQ8m(AILGnC)Ei@1#Uu5XMy#j`fUZOxEWW27G^&v_mo#&bbL zCK4q?xCiqJbb(!PTo;AjV`ZH6`x5QNe?xU?&Mp&)A?_E_e?gxl>}=E6^kQ-Bp(Dei zFf0c%VjddS^Y`cr5UJJSqo17=FEwX~gGCpDZUl%^AHDtUe6CG6pb8b_(B*`Pm4gY4wAR#ejoeStC5yc)lNEYk!L;`$Ud|NR7#Xm+MMryzxB_`nD4tAlOs*Ku=7_!~p(f})APN6vw}RVRQ!8s%3FSRuzkV<|eRyCO z%Uip3L$_AqcsM=+GhvhHsPU5M9OBaKvp^DBuI7egB!44&s?LQMEdk*T*#T-Tvm3&H zHU*zLko8vm0yLi|eSzcPp{?70@GWEdAADOw%eNO9X7fbY+cA8V>EI_HP#5e@R(Swr z0?=b;z-UroYbCZUCbP^40Qx3md;{ZtEs2d#`}K*5r=OLk4uo`j z&Jw}LpEy;sq=$d;YEqnZTR(W-kh|6%BYk?ax!Tz@)(nx!@Ed>js0Dy6TRFl_7uHKe z_?ooLH21kCi6q&_OF)Q#oPyvFv;{G1Wob$x^))#HrmgWZwy(W~cCA}|8zFC>-yEUN zQL&)yAyQ^)mhHeQPd|$91c>_O0;vXvTf$d2D~3Gdm5>LHw|lLt44!X_hNv63sl$N% zqI&rg{XYR#5s*SmAcY1TVY|uBKNM498t#uRU$ZVkp+8TH@MCP%75Wiv8C0q7graho z6}-a-qGuRVG$RX|&io}@7qI%_bF;}w$qcm1T*1^<76TwWeUkbrO+(LrUelOScsU7F zEn6c`9!C2EXMNWFXG-Sz;vrumnb*8iV$jt((997{=01qFkm+nebqcquY6t}yUmDmG zy&O#cwDA}KfkI87?__WiEA9)KsJAm@1#b`_p^zL+bpG7}ZoO)UaQ{r_m@8fa=B?*@ z=W*njGCmamnWQD&rk>l`;kg@ zmO&=KWzs2qldagS7fa*#ufCko(qB*iiqI-+xOxBnCzK|c<-!B8lF0Yl!S`JSJE;z zAqm_G1Wov3)TaDWd8K{Z(M*_RX##q zP$0=mu|9@AU#+ZGAM5IPgZB}K&C@RAxP%Vm5Y2AFh9;nW5OkZw^F0^8SQwCnL?OH% z?Ol0toR7Bw(?#SoI0JypReP`>BW+c+L!yGWmLxH<8;?u%q7&n;P-+^|VgVWfke8a9 z$Q|2lB}20G7*x^%V-YdR!&Fg4G@kJ1A>c8ZRkM{%08iFg(?hutlJm3kWy1*_>Hl+oDOsTInt-aM>wAqHs;CC?Zp+J@fUA(AzBNu zdiY-D1@lEU85hLcV6u3Tl4c`iS7b>egP4B`^(E=UD)W}Gjv z`UW#0?x4hw^;I(l$pE&|>;FJ8=vWhnmob?DG|4ESmnJ38|Ja6RLOaXYS_VBDKSstn zzZ}Hiq!$NJ}G$d9b!d!UX^}GypqY07f&l$^F(pHp9IF~~4Rf^k; z=V9}9p-nbe7ryn@@7oh>XwtF0ue&skYGF7jkLD~{4lhLT6Kd1&BUlA&4yM!tN5zK^ z{)Y!<^2>K4otx`$z<~MK=;D+wyUIJhcoF1ekEj7gPrS~+M)s|&G^k{KrBZOELabILyuE@Sg0JSFt&8buL~F1hA~07!%iQx@>J~p7aOAG z6y#r};Kiy=jl1l!RVwcVx@`k-_u2w z5l(sU5`Fq!Wum#XdQhd>s(nuE2zSHx7lcROGbTpp2#fH*x9>m79nDxQTn+I-JEC`W zDxKqPo#9?@%98doCAH9G78*0gA`}LGkl)PohU%eVr@WEyKvcr^dUnNNcBI7kVRMt49J@DTgxfyH1zu3)x6NJZld=5@I zP(%c@ZkW$rjf(0&+njGz+1+(uMtg>$KE(`*5pYVB(J`PBnL5wZ{!>;TJ=p?~ef6Mp zn#Iv?P8T2#b!R~=O|4q_R@OgYYyQv;2_C6gfk)x%Q>iNgp}Ss>Y9ARNfaKhPW`O+;AVzuSpC`*F=c2cYgYT&kR}(25k4>ghdY)vi){8*4qIIisNM-Vk(?Qn&Dw zS)iWwn4T6udWY`&lQf-wDjWF+DVwg>K8lJu=zz>a4#buS=0L^g$cu~uPx~NOeEsT# z7Y+{rn`{WMIn<7OpzY7GDEWyYQx5F6pH_0>fc|*_@DzGi17>$uM8Iyf(Q4Fr0G8Io z2AeI^JuI|;f+4_0|2xiQCvU?|^%c))(Jslv53lY!)gqzf+$d2$Mxde{U zUg$*h#`Js-w*x!CC;haPqwL*zX?+2shKBA42u&$9CwB}Y=g|>7`&jjoD{AUrVs)C_ zuI&Y~8W3CTz$VLPf+aZ9buBJc$15}?#K>tlKKZJ3=c56zbjcp|T(2Mzut#b(G%!mr zLzai!`e$8|BZ1ItZZrwqcI?d*MnXKq3bn2IVFk=ZZLeXzr7$b)u}dXKK$WIilphfN z0~FIvPid!@=I&Wn!jan8&QqyN8Jc|~cDO3{3_dzfgwiJqAU}T6G(S8!S=+BVnE@V% zMA#%@@p+Wi-oonjV0!75r$8l<^I6uzs_UIJMF?HCr{4PRdLf`ZqBORBOH*o^gsx5d zS0q%ly>CZ(9!?`dI|39r&}46^DaZG-9=$#p(s>fxB!>UaE|gTl0Sn`Q1nJBU4rWNe zcW(GX+HXgjS|szU+6YTdLZ+?f#TMxlZ{D|KH#-x+l?3GRXYM?GzZc=w=jT^9>2Icq zo_Apc7*AF{KudbFj~X(LGE{{mCi&a?#E{uMIl2um3kLVSl49)g1&!cWgzhc$jW@v zQy+sR?Ex1}kBtf3^dgn9MR@`La0b!J*WqU!pKZdz4qeAGCg3f!GhX4&fB$TJlOME< zouisT0Q`WdE>jbb!@Mx`6nDP^=5u?sffw$F3$ICcNg9-4W1Glh;HfM9v-qj2oHzh; zaPM2n!_J1L7lVuT0%j1+r7Su=!`7dG$lR4K&Ttrjbgmd9x^b2X z*EhvlGW0)X%_sMGT=@Y$O)SKOE2#95W3&N(~E$=Ny~~{UjAPmD!gu? z8>nlAm*bs`SB=ldkH*e?;jp;4`4>_^ezj|f1cb`!p>ZqbSnE&%AXT;WAd!r3l64K; zs8BXt$_)`()?fKh>x)g^SJ8Z`Ys0mRKLj+ zw@!K8KG(|ucfi_~gL7UsJ{Y%h8UcC$$9~W(a3qf|(sVuOzq)=iciJC78Mnd)U9*1& zq86~rFM*Bs!~Q88D*b5Y=ct`LEl{ATsY#y`^H+F%`+(`OpEnyk+I-m*p?&rzG6$eU z;9w2`fgX;g#D3Hg%JQ=x;r$vQ0Ct6k^=EiUA^}dH-7S4KRF*cCEG1Q=V$t&pEtBc000hP}C#K=spZc8?D#7D5HD$hGgp5k=R)%R*1Pk=a z#FOWiEKb$z<;L1fWfk!XB4^>t4X$a}t=z8t5PcvjZ7s^W0Fl-Ow#&Zfn!)BovBXbJ zLtrNd0is5DXJ#qb^(k4y_n!5InuH^l6R3LSUAo6}zcKW0uM)q{_q?|R@;$T&zzo_B zBjt+=5GD4&6{YbCi|4=Ubbx5t-y80Z8cZMeQbLp(QP=MQg-!mhW)w!viG^9KyY3e&I-H;H`y>M60=1Vm7Qdq;YkwR=_pu~p(^Cg?P6HH!xb&RJ`D z(EGDohg|>g0+R6k+?A1^g; zL>GP>k&NF^1L1bGWf;yFdt#~zSbJ*B1Gkc3{sy#qNZe*~ZO1#B~oVlUZuBDla^i%8fdvR^$n9^wA<@JkQ9m!PMDb}DP zY0R+(FJL*-HcC}`h*jl(ber$3mn31^kL(2EPtJCq_<;CV?FG1Lb_+PtZ!i@{@&DQY zj$L6mT43+kC)ZHZi0twue?hvgsM#vul*?W(12^r^dy<~Ax{DxgSJzJoaQdoLNC2w9 z&6t8HaB~1fvAPoAF=DdH0lD8~OU?vm9g~B{=}T1}CuLl-DF{IQM_{s2ctz|MBqZ)oOj1FW_f3LiodJ{qLuGeIB!-8rr_| zhK^zjn%RIR9k$R=IJpAN7vY&#-+H9FXN|1VFy5JwJkXK&JZ!3uim3V+LW$$GPFfy; zqijpEonuyWu0gMv${hqG77F&!r%o&UfjHrXUNA4T99zKd10Ab|o5)K{ z$%n3Q0Na|YySq>R>-Dy`y437KtC;@{_ml99K1_hNmaY`&fUW>w_`ui;eQ-P~L0znA zK$|aLz3!hQv>QS~KDv#YjUlB2d_k#Yj0E;G7Z(L?nk;u6=y>V)8 zIqEvTZ|CuKoF-&QlE%ZVIRD_Lm!ekjMYjP~NI&uUy*bJ!_y znN0qG5?|7cRuRj!f~ zwNFt+d-(p`=m2na$(MIo0K(xuLACVWw8GyHr@*BV|6~DjXtwbPxih@xcj$Z$dNA_*a~?rAamwy|~awu-;@<4o)Hk+{3Jnz*oI^sN!hi;B3al1CehY zA#|7>UM{m#RO<+^0l&gKwgR}jLnl+S0pcpc;~vTat<)CNN3E|%*U_!db5pal>*H?% z8)t1FNqg?F)0Olx6WF)9;YuD|yeG@}py~Ju_oNJV^wnbr*uJ#v9;|Aqhhz}B?!j_h zCzQJ+MG_og(>M?AQgc{VGy^pnUAAGXrS?lbb1`)>#h@_4_0g{Gw{Zu^ep7Nka5Phh z;zWmemtGw(Le;mLIAt-+}*^{YDOi{+!ji2ukrg` zpYnnz=?nKXCpyh(!i~d^=W)~Z{~LDR-3t-bGJnBHk>9&OMj4zAB>BcI0+ZPA=>NMy z3k2uUO(&k;Lgqb0MP*NPNjx1LE6ArFMnwqXe=|#yC<($umnuU*qd^M_Xl(w1{QiBW zMC2=pXHPN%sL_TM9(w<=vyw-v@*-*%h|&3PtE!AWwL%4?UW^)SNNy25;G^#XKh>4>j6p@{ShPNMJXhS z%QkR}s|BJk8DWfLu3%3oY@|k<-}o_ht$Md9tjty*+F{?WMj~ znL`vpd*4r=D`!t@k)@|Uab$N}dFMR=e8-0u`h>6x_EHHk>$ilr{NoG?x@mrvg~Ww4 ztL{@AbR7{B-@@J`qG6Zid7v*uGlqIv-AYKG8f8h z%=+=Vfk8@HbLrF4C!xffK^LCf6=Ger8Wt(#Z@t_FaUslXyK~fIO)O%q(uu>v9#=_8 z!NAZ%6JyeU>m=6AZx(#se;*zd)o3dosl{969N{z49`Xf7%3+CPB-m@=I=_=Yb(LYT&OlPQ2bNls6OU&JxtZ@_QJ3YPTi z0+Rfb6BKN2*0NK(O>UDCAFi4rWo{W12e!G`Pfr5)s!ym&JgRqlM(2gJJZ^)nTurh? z)ksr|vp!!hBvOf+Zbq?EuRCm48RbJJU!Qo^1t>{{$ok`vZ^5*nZ!3fvwd5JGq(=3f zY85KVa*Ts`|wd$Wb+3#K%7k$SYzOMG&D#o{MGCnfUJ`Nu?UdT3?{_>5lnje3DLrKLA z%bCsAb&s(C+ozpjz`Z$FASJqxv8kEq68NSXuaRmCrKHz+Mme*_l%!6w0CL+3t8H{$ z+5ONs$*oZqsgYihrzV_(BE?GlhQSdsM;o#<_c>3wj591Ox;;;g=8Z9)n-u{J$Une^dAWw$1J*+jSw4>+Vfb@&tsN7hjOb5LDvKJOf3Y+kO`BW^n6_)nMYLyI$F0y9@U}m_s{jbd zZAU2;ee0&a zyX$$`wLRJJ^V8i(8$2s;=$m=2$);9+wllPnH^S!86=z)0;AgS=;Aget6YLz``&yRO-PPslMbSEK#yL0?Le8yn|GfWg zCVdX@{tiVl$X!5(4oRD0QBe`{2bE;FO4=N_s+LisI9C#fq?Cftzn}z<|0)d_zvmBg8-*=C>I?S7feYg>NVasvQV<(Q4Stza5C#7ber$!Jal>SQ@J!mBz$ z=+-QHW9UwkbBAlZ_2z2bm5SHym%<|Fsgq7M9Ir@}JBPZa^16w1pFPvP1rYlXw$ z{T;O6t#inN(3Oot3d8dI$)DozzcY+cU}p6hGoyNX<4lX9r1a5+Xn_og1m~`u8qUDo54hR}V?7B7m=oUw9Hk6bMDlb+22yZ#g?~i3>Dwv>` zVY4gP(D6sC(Y)Vfmh0QhWExB@(VpE;IcCWRsK3+r-RW6bRj^^xka^Ez zmwAh&+sAv0)lzkJ4(|Ppt7q$J+kWf)@sOR0=E8z>UlrTG(|y}m{D4Mt9Ov+~19=sBpxyMAiZi|o z(lRm{MbR{Cu6#;OgBo-U5ZUdg#l+iM6MzYn%##H`j8}xivK$f`^kXjT^}&9RB+Ny+Ydlw+Q$2 z9Gi<-_!)wPl3VI9Pcp9T@58q$#jGlvGrr zP?cu`x94;?*PVD2z3(>Dw~L4&B?Lwi$aft%4?kMLi*TPP&PXUx&RVt`(0XTBI<&J2 z_VjJ$D%CB6w_eCbxO=N}r6@!Lr{}z~bIF`BwJOX+t(>*1kfk25S20_2=U?DCTwz;l@ICjR~y(>2{HLcp4 zSZVb}3~5z^9S|&+HmC9nv-zM}1Y^8&Hkg4lT+;~{^0AB+3|XYAXXqRmk3uU%LbOz*#h|H_ftnuge(tn`j;=HgyxR7-*&t+nwE_Osn^ zw+|maTzU-BDI^~yc?lgiW1yn%G+1VgZ$0mXY?OPOCFeH^TxaqvEvZx>U}Xpru3|56 zu~&&Z3!u||PcKeNYjx@x#g=@UL@@oeeQKL%Wn|^#)H?HrMhe4Qu1>AH2S0ipURX{Z zCv9RDvH?9={`-uSw3IDR_I$zOYIp&7nsIbBed76VG{jCHhXOVUtL)ysUCNFdd4yKm zO{>9@&sCZYq%a-68cVOQLlHq%25u`KEhU;zFR=(O1#vGrwX|N?)K1+timK5nD6h-L zQOlJ08I*5K8y2JsHrn?8d0J32y93tZF#r3lG-{|nj={RShW^riNOmZNT`ioLtM^y@ z6@Sfi7>i}!>fw1=gLuf_A3uLiZ5j?n0-C!+EO-*$a<-1}P;b^|MMXs|f>P<9XlArt z8Xv0CP?L^+g}*GmyQ0|oTKVmn&gg$rsM-*nuXrhb*mj#; zvFTT|Q(+sw+E4}Eo0WV$5Z@w?*X^5&?9z2Z^<=(=oD6OL%A&gxzwh0Uz|GO>3&BH= zy3$o&)(Q8pyPrRh^CqDYP(K9WKhy&m``6F$2#8dcjg|R&z8vhkx!WvWUnJiR)ge_| ze;i}Vsv95{K$nXy5JM+dm*E$DL7gk3i7FH}LvtRScFrx`KQl5{#L)WIo$22V=6!&l zP3!tz78`WF)j{moV3Uq2MZ4O!1YP>Ayul{t(>ntV3hmV#KRPQp1%oFMhvc#f3TjqX zg>MyK709RwtEF9rBQ8#eE|+i4%<3$K(}0N;4s1FQA$*f+xxnwXs}AH^IedCwbGF^` z5isDF{`n&gZn+XdT91ST#sa`BGTm(#02`d19Agl$Xv%l}{aY39%eCiXf3=F+ato95 zi`cH9proX{v|0<+soz59v~o^c+!0oO1jOTLWbx0u%fAK$a5|hw-?aJ#By%)Zu+6X77@A(`lsZ{e63$?gl}GPt6w2rzHvW){WH>#`V_n9~4k znA|(0y{Vlp{fM`)v?3&UV#Q4+=%pSV8x@t+1m7tnVz;EH;nZNa98y_1aO&#nHivRE zu&D-O5>D+ecpsHQcM!!B0s1GMe`&n711%B9M>Mb{I9XR$mu&P8eEL$r_R!EYRPVWO z9Z<^ja}ir5=NnuYvgrHkvoDNZU+N(l3YhYk=^coaY0to(o9)} zxH{wQ4HE`vWccFoXS(EM6d=J=Qajzc4qCzJaAc0OUaMa-+Xi zN!=`#+B^`l>IPrTalgWyBmv@0;^I-H5t|hhV-le@@K)d1%bTt+7R0on|J9zDoqVa= z-eTNdBeI`)vZ)*tm$_Vmz{iP*lIE)RBC6e* zU0yzc+dw7P`{RZ%tl@~gRT9qT+s222!;39fHMY|Z8jZy|wHZYd`6X8Bv8t-7Co_10 z8U-IreR;iqN@9_`yOQ~`)@I;`04bz$NZeetP<0-pl`}H8={dHI$=zJGRq$aGm$*AMd)A>Dj=>pfwocDP>;0kfXAgS)c`^j^0m*POW8->i<5w$}a*ptq}*heu+0{k1c; zM@5B+Nw!9|_$P&=laTT<4YiJr4#3K20f@KRpn{s7mqsE_Hg1rMiA_*Yf0fp!I5P5? z;M_W;jH-5`oI!<_jEoF0GeY#G)aJGhGd42PH`eOOK222s4T8JmTM~o~+sTq@L~6lk4zEET zUk?27VvxOggDm025TuSDx^TDR$J?dHdQkO4J-J_l$U#p$U@ zKOORy*INH+G>w#z&l0>3XSa$=$_1bx8$|CL+EL)Bhuae5k%MDcTF?#Oh-+_lW%?VoJN&nMeiI-^OZW-rliJ z)Ri^&ei{!q&}{Myv^-HdMTd(Ea6BF?h-sI7g>(PQTakY%<@3k`8H!5y{BY&a8|`TT zvUR5wzVz9LzWbO;ME7p(|BcA8)5``?J7fp){@QIe#mXs0xVeFCerpk-@ZkMC)(3?$r_a_EGPSAoH>Ki}Hd`Ih!{ahi=2VIMqCpFh_eTLkQy zHjuGnq;-{o)pDH$zo$+JaFFL?F z>I7sJxNlCmX>Yt|))}bD1WeMa_P@LgN^hJl|DYy#xn^6z5ZBk@C&Px7vftLF4BfH& z5n5wC@V}#Q2Q~-nZe()tFUybo$xu*ush&M-xAw@Z*0)b4zI3T8A`(t4)|PB+ z3(R`KI({rB_z}7%Xl2vEG8^!jeu>ah>$?JrYt2reG5LD;(rwQ)Du3R_G_lwAgQjTJ zTul2^Gn30UOItEK`I1u!*N*;&Hl2p8T>DLZb$h9JBFIz(=5lOxl}pA@v>)f z0QXhnF`pRp>|$6W{6|naAR522$J^;?VNceoK~>Gb2GUe+;6g{M3E_Od>!A{zID4i`C-qU2wyigw1!O|K9yAw3ticJk(;v?Iy>v@kp?lb* z1wV5w9FF4}x*DrnAPcu+!`iM`$nQ_6+Abcj*SRZj#NViIDnIk}6ux-H3x+EJ>?@+llWBrFJR~v#tjtjI23OXg_0KsirKf^E%VFT`DHvTbL zPminogN`ok(gqDP`r|8$Bz(nmYg1q|{Avaj_~Z<}>@!*DIx`&W(B=wVE2q=dxGpGgzKt5C*SHy2Ga9-D>Iw)uH}Y|6x3w417Ie5@7ZqZt<50 zk2S-O&y@UL_YA{7?FOlrHCI#wzc|K`xhS$f7B(Q``AafOGBW|Vw8T19Q|cpM$eDW0 zlg$)Uakg8?-2#JUeQ7;v+~iLA9_>}IOBZVz=v4Do`fX_V56QN!zPp4B5|_CaBm!S$ z?5s5RMayj$(*1I}eU7-?c2`^US6cmLRs?M#N06^ZeIj0>#;9URsgv7RFY3^k&v}Yc z5b#_SrMKWfd5Lg@N~V>iN+0?PHaC|JZiO5teZ2Gae466r&iiY>?9;fT|M{t${E1S?&#cLEk;G~>VJbf3|sJ32r`^LDn5r<-^b!o-4b?Lb=Z9MAH zEJlq|k1$+nVHP;B!M=cE3$u#FpaM{}^7@*oKtiz~0Qm31^9^j#fT)utXk|pJ`Ch$rGheFkp-)Nu003v7tari~jVv;K zs@hhqvM4Cmp#Y4CRNAS%O#eCqP+tBlh3nef=2El*hdRB{-YcI4h3t-3NO0fGSLn2N zhf|}ezle(T_Egx9V&N*r=fm0|zb}wFt$mg!P?eM#P3GL(UpOUR@$L#hT6?4Zf zsjFa|tNEOfwzI$v_9!*`Jx8_^h)D1p7kkZJWl_knATf6jV~Rqt2K|AbLiEP0He??Eg@}Uu(42z)A^zIpHV^u0#N9y`Ym>_y0*7By!;Yfeeg46z^8m(AB zUN^-4$ZLr7QAOK`W~MKn&>Q_T1}a-GE04kxYnEU%!S!DESF!5LzE3d`fublPx2&ST zK-a({PcB3&_8lCoU@2})cSGvpiLMHsDFO!cOkx;rm$gh}w1S>>g3w#TyV#q7{UvYt z@VovJDP8j==FuDf%Y~BjGA@k@t3nkGF$pRk!}7ZQ1BC|y#p%c%f|=H-HnpzJ-oWk#rBIz`RUEje9JkgMqa7HzWVmmW$WYW{fKrkIg@C|Kep$D z`N>lpu449pa^%#Y|j%A|onY&h%F>JTFRIuAlt@+v z!9*f3ltVF{b{@TZD4XVZ+``Wq$#TR?=6!yGcST0=;mAD|wJkNRKxjixH;CM?N4;*! zZ9#APZLrv-Wku143{SAL*zJzAT*Bh3RfrS|6roBh#(#PlkM3nK!JZ4&%_ELeoaFV5 zD(ZEr(Bb-31j1Xgl?(FOxZsvf_Bv0KZ$q^4kCn6p%Y&^=3IQ|Vje8+!Ks!9r;@{v~ zo7-N#@|}nrKwDZ$H8Bq5wuQjNsfDvcHcca1wOuc#>oPA@FWCAtq(8!K9;2*9+?Q+AFz z{F+8BA{c*130#X+A0I`L@C--;0N7J4FP$RQYl(Jveso0XPCp(6g+lUwuZn^|`L#Tc ziEOlqNAL?INY<FG&np{vu-JW+N&i`A*HSm$W{W;HT%2* zORLlQieZaz$<-l`0r} zDk8LPVlsrH1c;&onYtNmWOC6RY+uI9E2xHNW-T4;q(SQeK)(pYd~gJaDPdzpZZF zIdEgF%b>tT&c~ohn>uJ++iiDdl&teXe}f09ki#sQFhm zH*x^!9->%MXNqVyYp3clC;VOYAE@;Ke0GFR1viZhPOw-fT1y4s<8ODrWI_PBN8)n> z!v;~M$H+)CY!N5zwl2#t$H2yN8q7fcl4UY<<=q4Z*j0e;T2k_!fv=5zWRoTv1; zSrk~KfAa=hJDJ?K09n0J=5!2O5!bM&nn=AVz zLw$!jT}Onkmj7L~;1c1zd>=7pio7BsqL+~s0K%;&|L>E%)opIJ>J<#ZommmxzNNXj z)Jz7A^15|5pJYiuw$t&`C$Lqew|!};rFjE>lb^o?5RmSwFU(Cda8Rpv3X$fS0)bQ; z1hK-EPtd*{wBPZ{6XD)Bz7B-8Ft`39W3BB;|F2WSz)?rYFz1n1i{BL)0YTBt6`ol# zEi9e0$Gy#Lh!_H-Gs%HNe@a38?t7}8Xb61$8mscXs}f`k1!Xu7aKVSunqm_Z3xQmZ z&3^L*uS2z?|5rEoYz|bnj^A8g&(5yge$%qe*?a(o&@z}*8)+@%Jlt7xMYBG1zo;kx zk`DNIcubw|uY-R3UY#+%eai=}fj%!ggpVM;YDJ^L)*w-I) z&zc7O|5avr6bR`9S)=LFIBLODE5g9Qz#_fbD%i0Fi-Z&*0_x^>>e3*(e&`Xm=EewQ zN!;wA=X}OzASxOfjXc@-$cgO}sKa$5sDE&9QY;OgFt9!jQXnZY2~?w4tAb5)M$>~} zGX`0dl*MXI@j+O-6{3L{)({w{EKo3Ds^8SR?py?@gT|d<01ia-Dr_4?4Iw?1`e#XTSzvw7fkn43@70n{aUfPmA9Dzsb*=;CgXQU z#?ThLg-U>=;}o}{Z&BUSc}7sgt1^kugiGX%V@{B3xjn1uk8621`j1K}7}Cs96k~@E zA;kem4FK90IXEZ@FaHk<`+kaW+o=!b$A|9~YkmNXQrb{5vOScGSf@Yg*P7OVU5f8Y zd9Y_%qEcX-uFF+yVFd&aog7CPIvY?L`=2ONe9#hnpJP{kUtU4B>wfb)&U&-(#lNh0)qhY6Iq8Z0 zEh-Qj*e|TWzWT4Lss0_e=Dq&Lm_j{_9h1bRwSqr@|5IRz@-Fb7nBo7eipoOFH_w=X z3KNnYv1sPQ`NogK3D7NERG zFc|q)r=Z~-!EXg6W1UauyxYB72aq>Eht5fG{_|}*p8w4N=YLQOSs2RpKTy4#y}`4D zy4JIjV-N?F-TEZ_BbMJnf&CTlDckrr-T$JXP>%0e{Euo|njdRXs~c zf3|}ZtJnU_#RXv-2}2gJ_UDF1Jc(Aa(O8}N{El?FlUPL(0iwwbbP@}TywTv1^Tp>n zU{Ua%BEmFGfBPR ztM=W3S(bP2bQ&PEegFuE1VD0w)Xth+watG(5vQwx$olfz#l10-oCq685SGli0^%#e zr{FySqs?@88{4Gq8Y{2>fxXAgSuHPB%-0)Vvx^O9Z)OJqHI1;|PUav8<*HfBvBFME zF1^zECY@jOsXB(!!lG(7sK(PG)nV_GvTm*A#*9Oeb`!Fi`;m}>YR}6o49vLhA=$C` zy(BNhiW9-p>g_AT@soM_}M?}f*9US<|}RFTJ=s+%cwTI zz-hVAt}oWY7*Bsh9-f+BN`&buzDz+&`+L+52pJ!a^iMd69ZQ{e zQKR$)ztcF5FDR1Cd#&$>HbG}wqcrNyIDXQxM!j%6ZVVJl*BeCXw9tzo_-L#K6CA=l zctru1`nM=HwkM^iW0<1q-W6Vrvse(CAW4)df5THIvxzmKsALWn2{0e^Ry8U;@wLx$ zpgg{)BFH#q=q-55PlN@BH@^THT(l1v_CfR!QG-2>GShc63dzqJC-;M%fqgDB9dzbm|_*Ma6EiB)wRr!YW5YA!NTrMQt(JT+a;tAERXVYWH4 z7CtoD-R9F_G8~QHa->|3=Hn%w1Z-Z|=Aj}6Fu*>ED8=6Mmz7wNbw1x2U$zu9sMwo7 z;dXIVrR`8=u9f!Sk)>I3V`H5-H_ZshZ!+YqN=j4e_fyo=}J>KQGo=(B9$YGc@ zDH53hhFafXJPXkBRZMMJ4wV~siEiC{aKSCe@C`YzYNefi=ZEAgbO~+GMx$?$i5m?m^Lc^O<&LG_uIxdv zM~%K)6BFaPzUE;0O>py^dvD1MofsgD+yymA9Jd_y(W2jKuOyX0Z7>Th4Olo1X)Vnd zX_Fgk;ZD1)8Cz4YknOqL57up<)Q>(<4}Krmgr%=CSN>%LU(+r(E1ESZzl9_Z(3JEu z)@nfb^bg!)ink$c{#)QHAhi3-Ol^gd3+%Y!O%wEK{IyX5RG(qDsz?P?;80FjdLf+V zIeNq5e}h%S5?%AR&b2{JUJ#Ta2dvB~Z()<(bHpCo#8f@m>Bcg*kc|LiYG@Z`0k)|` zwjuTIPC`5}t9F`je)7J?gW(}vU;9P4yWnoP_n1x zKGbF7aMP^nZY9r}4G8UbIBQYO7C#A@qxx-sdD>~!a;E+qJ9TZUAF*8kVe!94u#dJ% zw~AaZlLUhlJcIp6Dasxw5stNo$mmqQQP?KRZ)|Z9ubX7aU0=RLLwo)rdV42zO<> z@u>VJVeoJmWH=mXLy$(y(dGM(rDo=PEUI_1c>O(L5}7&#;;0@WGv4jCv-B;%M-;^f zb8?3JoMP|Kf34dv4-xT@pbySJ30FO|l5-_QKK9VLkCwWLGW^2uxk6G#wjg@5CmeYn z<>jY7^xsE$WDGCur|0vEHkmW=*#GKB%8oy z(>?U6KhPWhGD>7N!G!%40gVqk*1x~4=LA^e^ET!c8+WnqK?-twOL@pP+fg^8alSeF zxyonw#APQ?U|i&K@H&@*xuZ$vLN#Os$!y$Kb|#Xj<3k@k12cD2sFjeO0?e*eNem>6OfNJ{=E zIPHKs@9%E%9JlP*(#zsd=)jK8o%d zg#`CM?zgDkE`&15$jTmWG~D!Ry*YtW5E0)d@%OZi6jbZWWo|MQ>>*cGiPyvLBs`kO z_ivJtTipRK_$<`NHBrvp1ZKbw|i0zb&@ncZhy|fa1IiTiBL8dn!tQci`rk6BSqI#7( zL-)t(BCJ;WIcE2`^g#{!od#i%{IioiF3|`pk_5gw3`j}mPn#PGqGAJjY2QY|_)Z{b z%iV|JJj1b4CasW~P)ZIb1CUgTfyZlkQ^N@5fdbrOD8E$IPxe;#Z@?Nx@~{E?Iu|4j zD9@i%&$jmDXa$iCT{dagIO6Z@r-)7-PALZl&Ykq7;$tTz=W@oF;_tJ$iY*v%7Okjv zvHi63KO>DSq@j?o-I_F8n0_P9rVyTzR?5mA<09zypT;J>9UFT+m?Z#&xM_jra7$7^ zim{0bCZ}9POv-N}bDe2Oy^eo5X;k3n3Ze@T^9p`sf0-{GU99O;sC4#*fy~lscFr|0 z@D*=*39z^iQxYE{)`DxhYM9iJK5=*CAzubfF1irTFV-X|cGU z-iBsOsPpV5dMy%`kF+Xi_Pr8geI6uq$0f0kuLk_{?V;IT<)%^>m@8j+n(m`-iIkk; z`r!8{=Alp!GzJ~g;+UOYPph66%B!B^`?Xa!Dj)YP?RI(sPR3CkHBpcPSIT7e)q@tE zwZwD>iu^_`{$qi3c4?c#wGV=`!YYFMprQqAN}G$54+~SrkuU?YTwM}^=uYPF9fBfA z3Y&U`+ShKM?WykxRBIT&*0tpvZl1W!aomPKxoKrm!={iQy_;=^oX6;5S zV(c)6Y`=vPivJkLZ(v~f>apIB+VPr@Bs&2!`&t~;n&IJJi4P(o58t#rk;IRCJ0AO{ zkV67roH|l;?v(BL%2Z~_j`9o2gQ{t&N_FOEbd`rW-ms62jr9&DQF)K9IVvwttxgLym#1A-7YA&67> zKsianF*+_xTb#Nll*d!E8&^uu1AaZKTqY@!uF)-=Axy|dMEwT1vR~{{Vs);eYR-^i zxRJS16^MM0)qn1v`qXg=cWO@ANUhSo4RgnhXRM(GtqG?w0Dc@o7RZIFd)5#Y!(!S; z=(hHYR#E=A|Q49}T zV@e;k%AHJ-P$6*6NDChw#xt4+?3#1S$MiEb(7+7;Gqh3?6fQW9&%DS+vkBTVoKnC= zv3sseaqwm)C?%zM<&>^+ZlD$FUfzDk+f4_YrxGA#(AR~xfOvJN&GF)|cxITZ}mEs1)@_=ZY1XhiDF^tuH%G&OWx2}tU5@=gA4N6+%a*?bF_hN4=AVst~gUskEal& z_{cb2!D^lKzNk+O6T$pwW?Dt*!qsMzzoa9nvv>}#}89!Ds=R*N8u82l1Q9v<(8<<-?LBa&D(Dp zl{Pte?qCfn`x@S5OO*FgnA*Yj^BAy z#*`0YW#jPD^}dKy6uNj`NG=p0)vE+Biz&Z*Pk85}(}*&vS^X{1dsN z3~J-qZ#*YE`91CALCH9@bTTI+e|jhQ^sN|V3a42QY)y=+0nzHH zr2nJ#m_8!76IM(c zDe_4xPcQ0-)o{?Ie%sNe)Q_=sd?aeT5LMoLEG*VbzOq=3XF7KX+s2237YALF`fthX z;g(Cq?N?aQ)2Lg&XMnuxW1rXP{di5+JA5qsWMfULwcS(UDfj-s#kJQ$PFH{UsjRS6 z|GK7=++8W`N%$}(Na2O4MDv|2R`)RTpM<6Q(OXH*MH1ug4pVZ{YY3j3Am_UC?aga| zD~;MSV%q(ESQNm1J?+0ocF#=;@U9Z5m-pR>1rluj9EJQs`D_c5K0g3!lWIqO2gCp5 zfF=t6-tgB=8+Cdg@>ITg&iJ+~Tjq!G5{|mkYyTpu6>YB<9I$B0)FozVk>HoGzbk2- z6r6vXXhOO`=s%!^goi_4zpe0N?t!)s4PoSyzqqBn`NeU9_H%F`>P zHd;*7-o5GzIK8QUe`ZRu=sk-Spa|$_lQ}1L;8ehDg|>IQyt^Izor4c{ihg*+eD{y& z?>d}ll^xLDW0M@Fd!mzHW4j~0#YK%C@OEvrtp#BfSz7$eCDY}Z#DlLWu4J*{garhD z8`P=hIMvYYLq&cqEUAQ+UFlcOtz%>$bRNic_m~{=p*!rS1g?{iCSHnSQOi6b7Bl~< z-wD=DJHIVN7bE8aMju0vZ~Ne=33b%^YYz$vj4pF0k`yZ`HKM~8YYS9|gDa2_;4qRd z)?}el59hL5Ro7)p@y8W1V@`XSxJ>o;lw z925=nVI!^st4oo(T-Zjpc7=F5j*0S6C~msxlr8{=J^3;VAiN9?g+TA#z-YeDfJVtG z)16G-H7+Sx4kU82y(DP`Wv#~WY9GhpAzpMejpgUKLY+&(sb%lct>$ z+V$??L&4xp6UbY1QfAZ=ZhpXWN&mnL=vGcLntp~N*`oE`e=(~ocGL8|X#;kTl6Pn5 z3LZa(JzMR*3l*(lcQnf`fKv{8h*(Z<)+A(Gq7AB)4}*Y%dV}L_=&HWXmfWm!!9T4a z8Ug{(H9x0^9iZ8+i;1aXbZiKf$e0`3T#S~{eiPDg!g+C;iJo1UDX~f?9sRj!uK3rf ztyU6pqD>fbm~LS351S1<*m+9Zoqg3Z3t|wLX%CCz!k;q(#_abcLi`fB-}c7+5TxsFlt?7n-SqPbw0a^$1so z^2b5_LZ}Sezb(iF)99)4((zSv^|X`%&tEHbJ?MA=c7{Gx;mVBZ{l|wN7Lf*1f>fw@dVwPQSpYzINH?$JR10u z;+)6L+`q%9dIS@zyEU(mj}X+|A(W=?e$Y{Bic8jFWmNo?z@0LXQj9IzRK58Z2NIu2 zF3bN6170)F`nu_Ntw;5fCbjPNL(fR@(ckR5W?$>o6nonbe%+#^Hbt0^;6&&Tf=A?v-D7( z)4`+Z$0*}%ZYCRY7UD&pm0Fp%7vjW?<}g`%hhtd1;yJAZqyBiDriT6Dd7e!t!hDwS z*=SEgD5U)7k{VzwvP=31lD+i!?vWRR)#Srd($h0LtUX)tnh)ZgXfN^w97Rn^@AQiZ zV*ZZiTjO)sJYcSCvn<-01-lBy@y4tMK$vrE(V4Qb-(0#uD1|nDGmoW}aejy(_<6*N z&KLaOqqtBHIPsg`)D8@~0@Iw?mi(x*saMUC&)h z{8Q*K5(Oq7*GYARPGIy?JA8qKPeBW z85Uh@n85=D#i$asg&l#%Y` zaexbprG$-Wu&rjCojy3i*v3bgXI|%+%`v5TRDUKV+{J*^6e5W}<-db*exdruiC*9O@L`l;OsM`y z-sFuN(5Y{Mp|D${=Yy z>qDeE9fL%~^drIF^LxOG#F+p-rUy0-o>Ypiv=b!-=rT3JeH0BqraamXv%)zqzbqt# zEYdG=Y^grld@kF0$?z`iSuo$rMVxzo1N@Zz^S^MaTM|U&6LvMD1ob9mNc+4BtM+B! zBcfTYeO^4|8&%d4pT)mCvREu*4%8~005?{!qFR1O*gwJx79~H`xPE zEQy%b&4O^L@)P7aGkpA>tc#_AJl2E+!@M3)l!J`s*cefkllfj4+(kSsE06dcG9eU} zqV~BZ_$E}COGd@`w-Fw(Va{&~OS9&5&F}VCH< zhk4)Xd>hIViLaP{oOsVaW-RX~olf>-PZxsLj7GotT-Vi+V@GQ!%d|JM{+c<9LEpQw zG~VywYCfwXLQ6ww(QJ=C{H5JI{3Yee!uSFWJ-_wYBbgVODwzL2U~!W`shuYASK-aA zAOqn?PmFwQZqs>E#<`P$-|3y?&Fq7u@BRKA?3mLsPSHX2V7UEx4!vp)wgw=0F)SDG#iH2V3f1 zQW7|Eh_J=BIb=`im1Q3OH5LEU^_{2j?JxORv(P6IS|6$<8RSXG0|0C}T6iPqlZ`_C z=SOdE{K~NgY4%6j)+4@%R63H-iM-`8R!}oo}}k~?n{NVIb%Bx zOkOP>%)7q#BeMnT_;&Cx?Z1W)W~Kl)of|Fby^|05DfyvD*~3$&Lt0PEkv{1#Jjx%U zfoG>*AxU};GT;B_M)n|(_v2OxobdOy=E0Han1_e>dQ0ei=UhR5BgZgnHY-U`-q%z- zA47YNO(LCm)ciE$*PAfD=T#5!=Lp1i9_PA29-)XoJ>UP1ZoqON&jLvF^Rt?XOqm}H zW@+mXxo5vjR7vMJ!oNx~wXdTI7@)=tVY>kFflo^2&jF`=Ly(!HbfgXQehNb*<)5%b z0{jU1FqS@zV=Xtx|JV>_SPzxH1p*98f__FXqWnmD-|$ znd_{6nWm%Sx{VWzToaWJANgH(g&taK%KI^+c@xrPI%<)jwI$)wh=^KEh9>gNeA|J^ z5Y#rzf95NmO9{T`Z8?f)NiCUZId`Sw7hzMY$(vmX-0#d~Rsr7u$NmsL)^L7E=(t68 z|7@WwA9(EXLaS#)i7c=VKXwB&ngcRX8JREWVG;QGH8CJO& zN#ZNjXv>F%)Cq4I|IE2Nhi0H>T( zP6^|DBnB5&;xTs(WdvKLt8%lnRX_*#jWhSZlH= zlXDR?4aq8I!P94&9sj*=t`q+G0WR?YELC)ucP*$hZyEZ?;hlk{)Kd8^d2MVX0{|ml z0x<-Wf1K4vC_oej=(MPf7Fq4!FC+Nh$5`8@k5P6F0k{vfKF}+EK0w?35TzV&+dGdD zrM|S}Wh1!2sblHVg8=7dzKnHcP&6n-j4XvqRZ09A;|zH<5B;7-L2b0@BbP+p)o;SI zPsl~FOqbR7VpL@uX;#A1~Ntj^TnvqrWxsB_3!7%)0JvaiX^4D{D zNcEz=vsnIFEw81)4AFckby3S~xEVN)F7aQSe9T{O{VyI@-)S|tcw=)l6Gl8@d;v^2 zyQJE%lf*A`_Y5j$mVZ*B0$8~%j)!g(N3j0s{2Mb0c{bBXV)D)o?1%5+Y&P(01CL?eHS+_MX zy}9PYKDm@yO|W2k<9HtozjpzXBHh^U<%$3&qCU{JZ>li}6gJIcIAfaqx1OKce~nT% z{(qglbzD`;{y)4ak(MrL5EKOI?hZkalm_W8K{}*MKuWs11f*e0BOsu3NJ&V8()F7S z=iYPfz30BZ_j&k}z4qF(X3c!&U9;BGhd4q_L#LCv71%$s5H({`%QVF`sE*A- zb@xbW@2TJSQ#}+~cZnuo&_1+EBgaMoizx>b^xoCzS8R$>!Ku{)EjLb zm1HyU;IB*+G4jlFC==2P=h&C636Js-1HxpAV&?dgA#X#sVMO_2KE4ZEL2V~sbED`U z1&!elm204T8Z%OKRA0w)!QVuq{l2)wLJFyNU47{KwR0Y~mu4uY8L61Q|7lTWIKPNQ zyOmM4Jm@!TwpI?qz^eFIq;G!3!;HL0DO2(S&4p)v; zkP&e|A$gFaneSTJeAb8a!Eq(CO0O^ogSMHq_l@83ugc>PWJ~-?J6WT=m2jg_XHv+^ zmmV>A8eE3W4WT4HsfSe4z9CBt2WAPLpN>T09HuC_4diiYg`;-as4k}4|PJw z=9{u6!cL^6ACKEG&g4v9dm|{^fdgXD^7kVW&;3ZoCCy#yG8EzzWqL7nU$G{C^EBVl zU${phc?Taws@_zWHR~b^K7*jIX9epqE{v~Q4v^s^q&;xHA76VI_#HX+u7G{hof2+j z#m-H-5KY#Hm^SaFK$8|6&pn=-th7gKy`N$rP?8Xb-6RcUP>!pQ)u*W#pb>^KsczOf%z9)*k$~>-Xiyq|re@^Yatz#>(yko|En?}+^o}J4YH>Jy_ln`D zFHCLs8_O~n;p1m>S_#<`eF%3R2*?Do6Ve$v#hp9^JkKxY)x~W}6){f)JtE!h1m*3oAX$b~oUH`N>B+Kc8e@sktG&-@fYy+&dPyY#+Rm zYCpG08G^Ors0MNN-TP2@?ZI*lLZ*cNhhNZG*YMfZZJZ4*>*xgE0~5uA;6bL>b?Fi( z0!zY9oz+N13b|l#MpI!AOu)}B=1oXPgwz+c(M!l4-BUqS#o4Q~${8-pxpVEtRgm9N zlNt!UWS3Nk-UyKZ`;Rrg#?2Htf9F`TrXcbvBkbW z8se`ZI(FtT^?4aQNuR89P`1!4?(1P_2EN>Pp?_*SU+t)B3 zk!h%HmXx-%Kdb{{!5{qDTQeaf4|i#})NmB3%Ym1T7!d{nmaU4phT<4diAHC9AMa}0 zyMge_P=vJQ#2!H$ma`XZp~u+mS|0u^;mImUF1R~>=du3OX=w&=w9nj1bPhw&(O?dbB9S;rCgu!3A)Db9R4IipM9`{sp3U>qR znC;IDm#I<0!WQa>$D^CCC!2ea04eh!XERvsXFWNOn_G%%G&$IPg;9!$3{>MPlHXyi<%dqD&)lgqr(UUhQ1vlgjtp9d? zHF?Yd8&@k~0L$95bp&I7L#y0l=Wm_-=`*=AEQTQom(NnYl=X#9koZqyM6bh+kTz2s zjT78g=-f7k8}nGm3a)W@o$#nuFt{z1t`6{^^b=eQ4$gu;a`zwlBaz>YDV`N z4cSeg&DIiEb71KaKVA>>m<+o*wSFN00jva-LMgOkQ4JUujYwqL7LZIao!AhzmIql& zGTX%+3A}XsiFPbGdY#;8IK#AgTk~}AwN@6d3jD`XQVUZksa7wB`)-^bPbOehNN?uT zwFJkf>7O21#%34Sq>Grymsq!H(F~Zlw}38yK|(Ay;dqUyLu6n2YYXU?Qv#S}qa?V# z`zf9lPU%`cdg5#8q{k!zFp<9l2HY|krv2XPb4|}@SqGkaYHa;4Aw9Y_moswu^9=F6 zX6`@t0jTw?odB=TAl__qG5qQ;hVI@A5fuqM&uT<$(&*o0;~}NVCtOI3^D`Sh10GAo zb&slN#ybMr3mdx-@EUOA{k2!X_W%<)Ktp{vE0FI;iyUlLHGo+)=ke%Z}OAX!hj=Ru(>igg|9>G}ePY^V#ta zxgNCNX_w}6933udDo)H8xp;;P&$T!w2%Ydz!*zVtUd{6;}-uB)%NQ1M+T`+WeTudas|LFEO`!G94X?)|>_TsJ{s z<`qAVTWS+PcKr`X+8PFYQ_Z>-BRe%pzj7aJQOJ(>1LUFB@TfL$^^v~PSQ^jJRD*EJ})@=BX<+}5Ri8Oa@UsH^xXCXWs(c$X*i<-NCfgcsv? z>7zvU6YE~rF&d=$4ZQA(F#ah^wuCwNk1W86w*|cW_q>i=EL1qic8Zn3SG1&$qjyro zMb8N6Km2-G(;`YXeu`fU2HV&lUF`Fp5ExreNu~10!zU60%pSpD!4u~c+~e=rqMi{x z9_eGkZ}$6;54!}d0mp|x$X*m@^`e?q9Q8E5jz+5(dd&3A2?O#V2T~{PH>P*gQtyOx zHN@H-o`D|Uroj>d-z-oDajhF$NPqQycm-k7qch-y2-~Y^BlFwclb-onlN}UT?PMgL z2fcgUC?6hY)(j8<8bq^c%nv(Sdbunx5Io-DCx`5faPyDU#|H1^4=oB?yE4pr{ntZB ztdtL4bXqz6fQU`>_)RK zr(gfbRBR5ZC&HGVpiOv~VT~P-o>BTPl3i0!T2WP?fcn#sQzM@B%&u}s#4TX=>J{jbeF?D1`sbcEl_ZCa+QFL}r}Hz6+rZYof6|qKD5JI& zcgZdc4MjzH^-p<6O7~2Cx!mF@E?n_F1lz(n!$nyolg5XAmkXb zg>|(AV7ZQn;`w%EoX4@IjqA6~5ubL_y5LB>T={$=*b&|=M(kskSLDo=bL--(B5+tz@?}@!Qe}*)qfA`1PQc#^&i7k zD7<{-xf`U%xI{r#t64Xgp$hoo9l@6(%T}sS))581J&zXt;sLqsoW*{^ofF65$@_aFT&4?)`QY6Ln0iYd08kX3uiP=Z3G!)1V3%&p{j!ym34 z;ep}^ES}GuV?NG(oos|8(iUJv3JrMb=4@@d|76DCCcvi8yih0XMRZ{*q?mSCw}`uZ z@WfJtkpP7M4i~q()S2}4 zS${_V_6$;b3`M())Z90{52SVAge`<>{Mzf6Wg))|ZvE;}`-ts>Ni*wE7)k)6;uG9Q z-f^PPIy4;niyABt2&-Ip?g2F(eTv8G?u%W*Ju&OZ4qNA9?%nuK*v|9t*ZO0mE)~-V z#L70Dx9lhWOM*=fT+}T4P zWy7Cucwq6Or`1gB-c29tJ&$K%e5PSh#UcG}wQyB|1n>vs{N(Z?47it#pW&FAe`@1L`f}*c=kE)jQ2U z?A^By52C5dZE*+TKC8~B#w-;2_1hw_apYX}gsRj(j#wxnN$=JBQ&|el{nOuW=<9~i zk@=ST>c~5D>4Sba-M?Bp@(fm_kgqB-u%A9x$aG zIwwrB9^pZ)5HxPr-;kMffnvy)zA@3Ct^#4z6jLpG@d}0d*((H}CQ7)D{2#_WSUZ_6 zuWTzmf!QGwm1zH?9@*Fwf2r4j1&|bx8qrR`Q%*b>Io;ddksf%yzg7#}4=@_0lAP+EFUDK^#|m|tOhYgHbkD~Wn2G)sM~!h0eI57$Tf=J`*d+kCS} z5(FS&vJP;47<}_Z5_VO77}?U(isy@d{56`D<9@4=^4?_rwaS&3u}gG7wGg)zog#1J z&fruFVHv)>5oMeoI`o=zR`cUWq$-DeYQb>xdW-U$K&xZt=Qj&P0q1DU2RVT8oo2@R zzBV=j(O#_Sw{2S6 zPsE!8=}|(qqX!C1YsT#QoE91wR3HQ`6B567IYf@0@xphi#75?NIuytyO9Q8z*Zy=% zv3C_nZ~`2hP?a?;drd33P(M;YXuIUSrSJhORrB4C zyRM5so&n&Qz(?=&*$6dovkBY8dFrFFL1eUp!mRnvATto+zSi-?q|xVWV_4SM;si3F zBz>F>0BMFSLe!?vf@8A3$#Ek zcPEyiVG^ECmV-8r>}3XW9woh5KGu4vp@5`06P`MvFj*x zZON7Y(v6ASILY(!H}$$>3Bkid+8;mqi6%RIT@$t8Ai$egT77Vpy9DM7fLlId%D*ue zx4BDLO_GR_y$|UXU6v&OVN7235ny8$_(xb13+cbyO-CyE_BY`pKMx*8{zPh&swJc@ z!U#p0g@7stH!oW%0MkM@E7TTSfJJmQ2;j#o_SoTsm8x5PIOZ{ypoXFL&1LwqZG%$M z-R^?vY@OHLK2*;m2rG@c-bwe=*_~=ast|HmPdmi!U(N-e8V_n-m*EOu?_ltb9sqd2 zvtK}{^lD;xn@&CTPm`0u0|3t0B?+0>XH-8A(sx7R;D2A=DPjN4)V}kM6*!-?*lxKg zk& z5OqX(p3Y6hs@~};{CeT=A<^%*-mTLB`L(cu>XkR^BRzwF>us|3&qWN=E?S#FK5f zuoboAEPiWZP4Dr??Fycn8&h)K5ej(1+?-;-mu*hB)4BE~y{gb(XfgNb4Go6&2*|I` z__?F5cDmRFG3*=h1X!7|9+OHN_6VR6v=Rgx7kKs##4kEB!s6Dm-xR}v2)sTcsXxoO z66dF3N?q;;K<_B2PlT5~> z=yxnjq^8%0HxC;GuTC0gv%5ezNMKS{!}QL`SAel`kyGq-TNdJ$Qu`D42i|xJjLiA& z63`hO$*>Du4ETXmreUp3Jt&$Cz=8r4s?x?*0M^Q(HV4D~wJ}l(*oakjxk<}73e2}6 zT?LMl-bA*u(T+!c@G$A`Lc=h!=s(<~rn5`FRsE)gK%ZU;QptS?ehV8RA%){cVmIk) z;O{yljFj}Y>D_I{gNL0MTbXj}pN{XpsPWD4Hq)Yu147uZ!Q2BQXenWJ zrro^^NY*f>ewq9iFt6u6(V8KYzzaOj9vhk*_m_4wlu+7r!|# zbz>n&zI+3t1U$u4)U+JET3I4mQ77^C%3(HJzp^x(Vj?=E@HT$*M_@w2+2IKllO!FS zO-fPKwHXU!p#npd=P0-$CEfxZ|1{)D+>h%@kLwfz9MRRIg*{w*D!nf8jdq26>^6uVLiV)`KIvFYNzF|CHt?-t=)Xp~qeEzezre zSKwE^^kgEaa9UVx)zWPJMzdmEf;ru^3qZ0&M|A|#2lZ% zo(^WCpa8dh!h;Pde`9Ax>@}T(RD7hKtJTP6mlJjd|IL-^>Q0IiW!)H(CLFhl zib9e`ND4(?JIKPu@8HT#8T`{_T!h^Kkkke%9+jj$zP?x6JqqttXn(Y%{xkHON#J~F zAVw7QA4$Oka%L_Y~Rsl~0L!nkz2onbq00=_^%=^9P^J-8- zb!~n<_lFQb?SZ^UGH$l^1lCk#o1Rii^i^}XO|uu2jL>&oTp(m|2a=mN6T>x)9*w10 z^93_eTU0mv4KA^X+2O@vVZqlA1z&=MXMg>zVJ7GMX1I_LMmM7GB$>qE$p9c; z!$Hbasw*85(d<~NvXA8({+mT0Hiaf=`ud-BgT*}UzvWp#K26z|5~7Q2vfO162y>EN z4UYa7@8Fq^>y7J>5+Dl1lKJoM95Sm`r8l>LGZ4j)^WZ;%7YOvu)R4g!i?A3rhU3Vx?{Lg@j(fcg) zLud9LRxZ$k0(O(!(YR;ydJ;MjAvsn!{nkn!Lx9wHJ)ien8I~21bZfuq3soBc=5zi& zQfLy(2r>W9bUy8!(g}|%7vYB@W-n-a&?hpq*fun2Wnv*2LmBOtbXsDo4N&}XC9q49 zarw{90bB3P!S^-@&OZYtwV70112$q?by& zs|AO;jhgKOuhIlwIh(CowUvCI0vJfT{N}J-jQ~aNvl?mkFztRBf4}s-_w#>?wKISF z_ZS686E(p>{!MTty|OXo3R@qMC2XX;sUd2_xb2p|ms#{8l&XymoLDED7~p_4@#JLC zlVL5cT8%WVzNC!AB4_Yqu@Kqf=d0XEuI0F*{kOEx74>Hm)M^Zye}jh!$64k75nn&d z&0Jf&6)|nCbK8NX*nFsip3n5bsDoLC*GG`6;r{!5kxm`E#ih64 zY;xtMf7ja`rclCUMh>t+bR`?Aez|$)Rhw?O_uTJw-V2@D30R^~LY35h3Z>Dg*1WWv zU*6`Ax~W_Sl{I0YHv6y=KK=Y+;~a^w>}Jm+^`r3r1C;$|Z4HdSXS#Aaks=>qe02+- zof{r~o(glUf7bqa&-#RPpzO-0BQa{ zB%e94YB_ihM=VQi26GXE`Bo&%Ey^CBeatQpb! zF%I@fyaL?b79Pb%jSpP7ZM(2daQ8OO%WEp#?^3KYh(X@Qmn_6o3B6o#_$dYev?Fa2 z5)%m{6_q*^fviiejq=nBgq1Zu*thM

    2}26Zjlnr|3rkDUI&)=U?=0B7LqeC@)%R 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ⅇ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 Tqu$?}(p8nUo|HX){^oxGQTW)H literal 32622 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�FPrK4w`W>oyaPs#I52(RsMGZN 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 Y)Z9JRG3xyLZRq#QODluQB;S7eA7m6IW&i*H diff --git a/images/tool_har_import.png b/images/tool_har_import.png index 663672a1493eef69a79b7df1d7a02045efdc3258..8692e24d5d6d71af3cce267b340a0d7c7a3500e3 100644 GIT binary patch literal 50806 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

    0b#ktBv%64* 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 diff --git a/images/tools_montage_a.png b/images/tools_montage_a.png index 9417fc4a77dffe5bafa799e8fa4ea281eebf5989..5f919c41388b953bbd75e04cf70653211c28c890 100644 GIT binary patch delta 69811 zcmb@tbyQVd*EVh;5`wfeBHfbGiihs*E@|mzD@b=98YHEq8>Abg8+ z@qFL=eq;RBA7|{d_g*#ET613WnsZ0DA(powexn}&%*g3_>6PC+!p?d^u7HsBl}x%$ zX+xNd-E8E+ms7b%55y$*Cik39ua}o7DWD>lV&|TMSCLXCCZ_iGwf?52rdW=A82AHA z-i#Lrn#UXuv)bwkIHf16@QF+-^~bsH-Fu_2>VYZs)J=q!F|A&Q%h|IRfu}1j0@7aA zzYtA(3UnPjP`)5rU8@xR<&h?i#Jxem-hXkkKjlFnaPQu|?ejX8u@g5kQGSBxY;cECnCjTN&@rR4I@qIu2AQ zGScLVn?#MsDfl_{=uMFkp%T<-FMZ0b`#DD67OK!?4if>+Y-VHYC?f~M_PqRRzCq%6 zWTh4PBEWN5kFCiWF-!V63QHxa=2*6^{buufot?94m9XczbK`h$)dnQ8c&?PQgn8=} z_|mJ~@io>m+$jjsct-P}y8HgUd)W6oOc4|ZPd3wuW~5J!2s~X-s~`{V-TO#BWPsD0 zoXTSLP4jjze{_-o-+6D)66@nZa8#F9G0Ci~{qG8HI;P54l{O-+70Lj1_r{y6P)kld z1^VeUzZ#hM5q7O5@3yL7>SPaa76c9bMpV;CgWyc(j7HhhkIuy9L6Mf98Pp{}78)^R-fi1~c6yC#m0mTA2B6uO!e^n;Jl~Y2M&ft7YdpjP|9{*_j zA^a6)hk|FZuE>)VL22KW?|n*QSW?ymDmK^%_rsY%QQV_3 z;CWmpbvttsOLQ@mKr%l|uT!`~Ps9nIg&g=YE0X%TD*qFBbr+pBf-*ryvRyWd^&w+4>&`-yZnL51_(OPSAC&}l zxA&IksTidNKFrLmJe^)Do%!KD@q>l>m1Y?ehvMj}Hnhn9Y3&YUxe5Z^&Nr|UnJV%! zat@O2`??AhhN%qqKP}M^n5)L^rf#_!XP$ZwTN9ePIM|fz14jmX2#R^2I_)gl(lcvRUhq=HF{EYmbC{2Ps&zP^+vCxX+I?k4OW{aO%yk`%yYXrRdMlj0|4BYg^7hvfksn{87{Zhj1^nm_uvsWo*?XFMSZt} z*cFnRb|vlXxSW{B;lrGSC?)+3(C!UpTpmb$TKOBbwZ>N*J(YWLKHV_AFR;%f5UofYFczs8KA#fm}VJkQ8QUT_*&^3SoPRFYg)8o1Lf^&33V z!F%c7%;co$Vt=$2T^Kd)pK>1!T;`n?O6x{?yzJK~2~t{Z?bk?3&cYk=)0o(KLMTUKK%U2VY`@5 zcdLC`PswuG78W4cj;2YK?OFJdDc0obBc;lceZnBI&)k+xA0wX7VOUvBq6XdhVLIPn zfFGDk=zQbn8}R7uoM-FUt~6fl7*Pt@oY(t-!s?2mvgfteV-54u`|-!?I=#^+9}M@l z_iba&TuZA-a;jJpIs^nM0My@wM03I?EXlWKvAgoGQ<*`H5- zSbi5=AIN}M~ z8J2yXIcI?RIZ(1}vG}W`MaM*&NLTgSF6rQ=NeRk|ts2_kg%P02j_0t#KOh9lgKs3w z9a6YY5o9wgNM$80STd$~GFJ9Dn&pGWgD!=k(-_Gvvn|K)rL?W*;9%@>Sr*7eAw6zi zZpYe(J=q0zeH}i7_wV_(|ECcXYeu`;b_`yiLEFm#4FPzH=6PwVNLWSM4|xEeA5D3| zA|0^zCHoGNPwU*Am1^_jO&pD^lDbdRWE!EN#t8{++Kei;$5&YuNGWkoSHcEbiC@R@ za6oMY4eVm(8f@6q+nM(0V96}T6Pk4@GNP%wb5@)e`N5zOU9@7Twwd(2$?*;FJC(Cg zvV6mB4SO=_@WL41#;I?Jk-DHE2)Du-3pOV1=8-zek!yD%Z3lUah~vewN~ zaNx`6+Si&f6^*C%A0nY-Biko#mqq4Ij&zc|9IDUc{Y{u)QT0H$rM5`dEqip+^{HF?Glr=w{E$`?#ZMzgI}9ahXH$)|@rJD*M7C(V=5uAJ)9 zV-3EhqL%ty!d*EcX^K6$!v^UN2@S5}P$-(P=#GvQyR8>0{bItvX;&54O}@`yeP7c@G)v z5&ZLIK(KabHeHa=d}FS+18!W8#+e_IvT*#$1}fpWLKO`@HV|LzK{@JPd2-x0uDz5! zFEOF$q|j?K2cNj)1(|#26*6;K)XXkKQ)MTs*YPKbt~7o2f_I?v<<8Uw4!8?~iiDE~ z-V~mk5|$Kd8^s0!`3U}z!zZR$uilFfjD)?K95MWX*i;NMh@|=HWD7?b%*286=AKyn#bjnTMv3Aa?qTLfI!u)+`)VFDy+6B>26+#3hw^_Nk@wW$?Ip}Z9Zy+hGY!+)_A;5+okvM$a_{4uL3xP+4L|aF7n7Jq{Uo3V2uYkP%#jM#=`JpnyBC}KY$}ku z-C9BI0hjvx=j4f3*(Rf?;R*}eDZ~Qr1eE;?VQA{BCmo`_4%c5}w>LNz#;?*O>naH` z*__^v%!>{lv7Qw4^tzP0%VhCoXS)fD=i)M1I1!c?CA10Y%I+?H5D*amcFUI7Q6``@ zjtoTkCY|v3e7MS39kH_Ldwlp`4f$v*-nAzjYON@N*m^^^zNz02{I}nWGmax=CjMIE z+~*Rs@T#L&8eDPAB$-yP$V^U=MlWsZQFn4~j%*CqRf1&NmLZX9ar)2A1z+c=7*9u) zT2JoH0E2!x0*1n-&pH{U-1b9cXKUkEALPB&Rx0pDZFmD|FFihztM2KGqK~tU>Jryv zTla0+olCM2V=g?!b(wMPKRYr{DWQf&FgzciZF9pNxO6SGI3@6A&gC1Zj=|IHOyVar z4-CLy#WlI?gQLQ3q2gbFVrDQDc*H~Jt<$nOgBTFRW#UVSUxo_JV5F%aBYy3!rc zlhqG`QBCy1KBp`Jv>~$>V3*izmGqLiXm*V2eW?uw2V-{ivd_blxze{2l40Rd*jcHsJ0C6x0J?)+j z)R(oQ_By?=ivFVMju5!itL?YStFTNH8l_!2`pjVt0ug17j}BehTBUqkzVpLiXo$LF z(JjJ`-$wRB@6PRxUwZ&~=thB)vtyKh@0p_LKbE)+IR^+(`z4O8yWu0&)eO1>5Tz#U z$qlP>5Y;qYKw?Xk;hyGAhwAYeG_K-3nPxc`!ZwSXw4P(T7(5ROz?f4i)JRZJ>-0wn#|Q{8G`pdxj(?=T6g4)`nt}@| z-Z!s+WpdNFkWmsIL$oV$r!INLfsbRs@~Oq!rdUg99Zw3VEZ*%+C&EHgztOnP;U$_l zCKu6?lS|Ai=pA3J*whsePD&M>|4gvDp(4fr*F%&M2ZHT3wCNCizrL`Vp!*^hTobtp zZ2w22=SjHa1PkwqZv|YH}Y~_nxU_sXxF?nD07_}NeGbm zwpJlmV8UCiu=0Bn8NO*DKIRH}pK-0nLD#-{D8Xhy*7J4GF-{LxYvD=0m@-p;VP%iF z+kxd*<9U*IlU3>+57ljGS@Nqd);QDB{LOP1Wo5>KWBQ~YgN>!D(VZ>naigbUjw_*~Ad{@qOyD3H`!4a$u_&7gzjLrUBt#C# zlvFbDQEz(bSp6WE!(QikG#Tm*?JI5lDJ;$E(xH^??AbU8QiutWcxNkmV*Q-^wh;o{ zC&AX1xAhL^?JEm09I)e&&Gc2!A?kVsqZqUB(o#y+E62dFRcA7ltz$92rMfpo!TpLG4|vu724>c1AWR9V_5*$m!l$bC8?r4{Ayvfq5sDBWO?WUvoz&!j_9X<5Me z(`-I30-(CZ1ZS$K-F2L1r>C-d?&^HBRmby-Oks! zaaUg4&2r``c+~lDXncwsGVPIC-OzakY5Lq_LXYn7ISS;3zi%D$Rjfm06Y z6u_l%sI`=?^c(fv8^s&Y4uQ_wUoXi^v&5dPnml!$bsx!!VUw)hB2YOm zx3?cjRugg&vpkdkce@TZ>y{jBw#)~(Sy(*>8zo453JNL0g3FkVi1cFlG!chU8#*iqzv;>G`jiy<-=n8SpzSr$1p zw^j07Uh;NE46=~_&e;U0d4WK^y+a&vFn-bF9cFaR@DF z6|17CDWp`bqjs^5xY=m;)==o$JPITUptq$O3_3{P{}}VwTw@oH}IQ{jiPhVIXPLli>_1fkHv7G>-rI1i*KAtiO@w zmPE!`#7Mu@rd&j(UZY;Z_N0qhgJ~Y9sQi^LY>IK8Fy>QYqzY#jQCEo40v8h%%|!pRy*~&X3i17f{JJP zXKaJDW_o#iSgiF&d`yRSW+_b0-z@wCRsL+qk?Lj7qeKtK5mEu%%TcYSY~q{cNj;{C zJf7cAC(I!<-~|fIVPD4uji-{3hBreQma}2;xK_inhN-EKniTMGx|s0`tmf5Hcb^Fs$%F-9Y$M(| zYXxeQ@r9fhEA?lWvQ`;l;E4dqU)YIWny_iz$!VnhTC%gwfUZJ)kWn`?QiAfnttllD zXaeh7Ug1dC!?~v;=4L?(N+pHv1OyX*6<_N7sH?voUX$d%R70_VRQy0@P^f3=fbTElK zEKfdClSQL|E14#&I1B%5xSUK zEB(%Cd8$%lP8U?Lz75(}Q~BaEDAfwl1|TdZ`ZMdV@s#oHiLn$%&GrRyLD7BSpzRxf z@GAZTx3V@vI_)43!II&G{-w(Ls+2CNflaX;B&viMbv_mMtxt~AVPo{4*(QIl6;E=x zzT*U9ET*t#fg;2z{c_AMpcsEz(*CS)LmitGhdRC}gJM9kz0~N!h9i1Z;15(y11)#J;DQXuV= zz&TZv%I&Ir5mP`D~Wq?Og0#Gl0rFjcIC}8S+ zg%SmxNUY%&HWO9H!*AA!1Bg$J8`NzW>rc|E?1932%pG=CBGd7?&LZZ+M8hl#ZYk z)+?Bxzb>Z&;tA@Ktc5{RyK#-);FBXGQekA(JvF@Wac~KpBiWF6ezk$vhCvJ@t3C>4 zfJbo*S>+k+G?_WJ)G18j!bUl=g(BuCa%z%2d%WxupWzq9zkqZ1W)53{^c=Az$*Ah` zug-}l5l(Kv>}t0wRv#;yf4xTG((iL|?})N9F`H+84$6(QYD~~qPc6FhRU&j6qwQ*C zH(J?U-&HvzU*dg>jUF(TRG=xGsGcT+QfXdgPfvYm4Ia!l2jyl-qX+lr+d+HRI&tLxhtk5ZJyy;jgvON&r?;N$gA}Z;9<8fovgj=p70Xe{-z%I z(k*{IWCcj=*vEoW%s(l|j4TM^F7Lc-Ve{I0v7E?HTafKHB$3P`2B#Ov<4C!wU%@x+ zdW~-nK8om|c=D94mF@r>xKlnF3RcmSDE7yLAoax@8u8Cf@$z~QIbJm+YZ*JN_&?H# z`4Cdgdr1-GY>9YI6J@jTW^HE>^b7dXf&r`WZFiPzM)3(0R@zLJvM{1(e|kwsAWv#Q zh>2|r?ll-#O}sy%Kd+pWV=;>wW}l1!my=A#i_!$I^2b@vhc8k$J9}#nbq_Di0^@V(S-}A~sgMK4i>{UY@xO`hl;b<% z8`6m+KmxMjVK}4AKNk!I)S20f7sZ|d=60iVwf&D%cZ>?u8j1wJS;xN z!PUpPgEiSO4}cTA;B+=|xy5pwjHl;9r*=SOnx4fAj0!>8+`n?rai$Y-xqmJQ=ao#b zd8y|Ns}r`t_IFmyi?#IG9N8tt)HliU~L%Uu}>$ zd6WQm5ei7|ESwuv`IL=a>*t$9&^>Svv7Hxqrv&m-VOEctQj+3JW5=mrDMW(C=~PCs zs#;Mj?kT%J)Y&jRLDKr4Trud6pASAnle`%t>~BSX$dTPR)dfUeC7;v{AcjJYDWdiS zIB#Zk^7ZE%Ty;>NmY-a|7>w`Pl>rAN=fa}F+^og1U-CS{(O1$UYh~Ms5xoCVbyX*V z8Do6QYM^e$=Tg6R6W8``gK#5Y;~NN1=DvA&>~1dJgsTMy1IEDv{@^kz zUIUE^VP>A$_z`(U8*CeaZ_=8nL&pR2wz2n3)}- z9K%?3QU0`lkLGg_VC^j)BK|CZhO_j08eP;$DeH~H_p#3}*9VBY#=dqlJ}(+4Y#*)Jc#2W9xSy1J4L*L1_K8=&yN8YnY z@>~mXSXLv?_|G8ZMp(8=S?;8Z>JjucvG@$eVS~#~u@w-I=fwfinls`LIbeq%#sjX` z5~^}qD6DU1m1dIc6<@*Z&x^w^mL8{ppghFKmZ$Z$5h$p^TiH6Rc5ccYHRm4A3A_T? zt1V5Bs;lkY*8?Hi*~GcMzP1^6(TqR-iJ!LP@;VHp zH_hg|DUVYfCy^ABsO#q*PTcfNlEM?niE2C0wQ=AT2wLvAeZlkdtHijvWt3BP75WNUz zbFeWbJIxV+{9a|OMq0;6yT<&oopQbu6$`a@Fwj*1#TLeNvOS}c025WOpnGlR4|>G+ z5Q@$y;!TQ&H$s=Dbme(NSy&iszvTdKacVowA*c0@yp={hH0t{SQCu~KaLVnG>7 z16%#B{mv+U*?!&`u}T8)i~n1I(*g00Rn5wnd(1o;y%rjTV%^T5CRf@$YmtaRpIQ8WO;Xtu(i zzg&mSCC$1}GCmtixR#Mv!-EZBJYG7rgDum}DsH=POb&ji+t+ig>R4~LeGu3VWVDh_ zh%p1b_RB{*^*#nW>-wb8qb*aGG1E`{9hGa%rbwaGMiZQ~kxBgqX{4Bs9aPYEGu)mJ z-d0GQ(j+_2j2HPs%)Hj&_-`K>9?kksb{7?*G@#3B7+|q_7}bT*I)>8&@`+F`7chpn z)p=OG)$2DPL6LD=DSy13u0)?8=tQ_~Gm;m({iP!{O*W|cgp3k|$;oag4R;v_X;ng} zM{=quZ?9}!SurZjDbm>AuzATJ)&hhRpr-(FIQHfSCzVcQ3VaOA#KAa+#z~z^DCP#S z^ZDtP4Sc%}7{1w#H0C`{tCEKi;Zeu_sA#cX^_Y%UWiS_(I96!F=5ma%0QpV-p))XA z!8BKqW1&0S?`&5)4SFeKLkCE3iuiQ2u%lf9 zd#AA}q8&4nSjL{@NIm33!KoU92iV-qjFXSYye~I_+kQdat26P+1(u*73LGSntFfiPUmz<7ijAAA_oG zOMe247r|tYkh5z}{W>R5BZ{x(>zCs)sb~(uFmjwu{`( zJKr*hoNaLpNneH*I1z%#bnsF|=61fQnR@DY{P;8IkOp_X*F+UVlH4xJz-lLMHg7(A z^{Z97FQPcV_GP72GgB103^+rYkszzYH${2~kx@@GL|VX<7Yg#YzNYcN*l^&Vbol6P zkOr8y1%<5k=F#J3qg;BW_ciI+*OC6=)ve9fCf;dQ`Pgdw&Nc>!H#l9@?WPO`sHqKD zgFz}o4JE!BRbvB8NCrZ_$PeOTNV_l}C32g}@8z0}E=w(Hd% zd(@6VD!~Gnxbb&rs7}RZJStBsUfm)(`WRGkeRceP#a4=~k1st>Ip`&_h#2ScWhYR8 zG`8ELl2E%3)lGwBMh#u0pkmM|GXUznH#Uj4g2>u}0yXGmw_y1FxTO_%rxdJ7J71+u zN*7l?BVBXks<>^0EY*4XeRX+Zg-{uFZ5CjW#=g~QAAt9Mkyg=XvkY-afsl!?gp#~m zxZyU2^iXmsT1;T4H@*LHbm_ODO9>ms%RZJ#UB7HT#_V3vNcFUC=c6c*6BWSbyrWII zXRgL(hl}#p?n^=pY0WQ5=b=gAyo9jNU^^K!JftEf^zjmx->@zVs!3n1Z9?IZ$n)Ro zF+X+v`g)x1$LL3YVyG)p6It5mOlj7CLH_m`s75Q@M}?UHSV-=&y6?y&c;I%>|_At|62p5~ZSXpzH< zm+RA`R^wK!f3kZ_XleqUR381gRv;^6H3{uGCvkUEfy1(cOJnlNg5hTeu;UU1c(*^lf>gRNg<7GnR*9&wRyX)9$1Vl<_^S!@Fb%?fgdg=m?>mY=&LZ*T}Q(e2iv z=tY7QJt9calVz>Nmo;iGZt&k+*%QNAwomj`)!jIBm|*}s2@;+INxvrP4km~V%zp*O zGr{g@0{=Y~gwwQ2`(q2-vo*49ZXTV#_m32iD7U=rHP56Tawk{S(seKruNIfUJvVNE zBRV8&m*qcF$@5>>`&5)rrazD!_#3n93UN=hdypF2~nML43UI%Hk0m4<43DwJ;x%QA0qbkBT z1%8Kwr&jW|Lq&>cJl5vZZ(tNb)EPXn*(r(i0bfCs16<1P36u@!7A6gUS4I9+Xi zQoEBoHz~SPR|(0nZopyG3ns9akFA++mdVANPHX7nf!r_clRhZwt0{Xa@FUuAFOr5v z&E>pHSPLF{_m#fiA)FknkW%n?cb7z6)dgFeixh}ZYJIPiWv7400wz#`=`lev&t!FT zU6XtjRm;({Fqf9?$GMlt!ACSg$m~qLw>Y4&KFt5Xif9ODziD94@Zfe#{NoPz?@u9< z{ol<09}W>b|Lh)_Eb2f7~WwMGsBYGMX4JSC4KL?t``|k zNG~??-q075--h z=`Of;cRzw!jOG7c0WSalT@7;A@rNIkFvA{ue0Q|$tg(pil3&Vs*9tTAzOL1JS_Drf zfaem3GV~prcR5-2oYk@YDqWgr_6_xaB%dW=oWMA$paXfEZaQr6L)>c zzNUyJTGpn+U+ldz=Y|8tP%%LMr{9^sb`N*OL1Z|rl!isrY_0=9Xt zp9lh#gDv*01Bj>?juvg0!<{}4Fnqg$xcSs;Ll(@wd)?yavW+x7doe4}SlO@1p-z7B zjeG0NNP6~*hPYMIH^HX{?zc1xHO!dU*4A_;)6WBiWG;msMGJ55Q?UUQz@$Z#%jKe! z?rvl5XRZe|xF_+;(7=G+3)cI_@ly~UXURe}9^+1v_)hXw{bHW19d0Jt-1YcrmwQ2v z+J)uPpW(e=@~(J>_qGX)DS!JamuPlN<-Q&{WgjK|Oe{0&;Sohzr70VE;Xtt2O( z$~I3bmpUX?XzIK?%bn0^0BDMFp2-wTEDxm4sJ!g(ET;s=99(_mKNP&jX|Pb(qhV>V zGdaBs^|d(NzoD(w(Zij7NGn>aon1z&N~pnQlr)YhtJ7yTBRG2@gMLgCHsA!+@}Xvhz_87-PUe{dV6Cr6eXaBIeID?9#~pq9s= zm`NVZsbuHy9A8q`$;3j{;F%bX*L`FB&?p&g9BRh(z~5KWRim>~+=87XV%(m!=qY$2 zCo2!OSQz@Acu!?8NkT4%p0ol-JoD^ME}v=%lFqcyH{4Wu?H$4rLJ`yV-)TKUK(@tL zA{Rrn#aKbvZc$Pj1B%<94R6Q|-}p&!{(uS|MaLa6NS@?>13j{g_hkW)EUqBx;wKD! zV+9v;eO;xrDeaU&kO~)G<|kvPp0J~^Lb3pqYW-{7K$t;!KtMyq7;Qx5E&X1S!cLl~ zL59p#6W12GWOLnkUhPlO0mMo99PKQyO|MjhM=gp90(x`p^j>|n{^uITZ1AN1LN6u*# z0y2#PXvEVlPA6^k2c!p{$$KPL0tY=1h=yZtb!a{tbV6)LH;J+0w`^(viC^&BmR8XL zb#fD%fPm(0%DLbHa`Jq&#Drr6)7y)#SONci!Id8?;h! zR1DXewUzg^ywjTK9Vdw8w>Bavz5NzmK>x7hQi9wodFm>Q)~mY6eq3teU1Z$B`S#hO zCqI&YA8l00^G~fgPUD`?$Wsq$4DCI|pzVWRmOLv}XEc`&zX?s}DJe%!3j|ZDo8~rh z4l=tfb5yGA$G>b^^q0Mgx^-JaPwpAM4YdcAP3yxyy5@7ulT`njCx-}_Qh={-3k68M z(^Y0EsEbf#&)()hRCmn;gtqreA==GOa4bGqMQd=Ne_i?rL!k z%q+p%!KEd07+nl9NcotKN)y!CH^q9tu1ze5oVK$8J8X6%O~LYq)2{CWZZ1Q5RM2Fr z%&c+@%uEJF1YRJ{d!2Dg0+22yWbZfB#Q}RiIcXW!N>vu{X?)B?U=o zjC5RZ)h)5{sb_AdRSS5eGzU<;r@m=~#hIFF3I;#K7)Atl4i8phW;&xk_2rh^r+~+v z{ebc87X_Ij!cqu(t)R16=<&4)1Aj9CA5$P&G7c;Sb9)w_y*P4o5vf6Fe)gNYsU%Oe zUgi>*LUkTnkKp&^LV2oC*E(95&rSZTT+qgF9A!qcP%27RS!yRfeD;`**mlwtx4|KO z8`(eS!aybP=HHb7P^d@8uq%KYu8W&EX?z<`Q+r*a)=c*jI=r;Qhru73Qv{%BFX(gK zbZghXoG5B_voLFW`+`o#LHXAn$4WQKP=;L-`!SIBrBgRo@fKGQ6;E{s=dnkGCs|Z9 zs1TI~p?;pdjXH6z1-M5HB3FjGVNAg4zLBgy7}R5Rq$>vo%uChxde5y5RcLeT!-YjD zPWA1p-YVyWoZ9<9KhV|_F1SQQ>$lQ7!Teqy1N{NJT9wEl3lQQ3qrl%9@$nsfR=2Vn zx~8AeY)VU@hQ$jwBiCFU^Xxj`q@R5*Y=ttcnOnI6&BxCY^;9K*=79-0C{FR5>lV|wlVrO+Il~(;KbbWk5*ttb( zw|t3*UDxZ5LXhOM;aST|uIYf+pz+YHCPk5c;jCkc8+&MU(Eugz*cQB=vqGSEMzJC& z#gSNLPanzv%`5m+z+$MKayV?cwbe=}LkH;p^^V-mrA*T4NpQ0vqH_;bM44`OLbWe? zZQS8OC(775f0ZJcDUcK5x!>>xh)C4<&Pw23_03`De;%c+l9})huzLs^FIL{9}DTHoE-`#XD^m~ z9?-C}CJlkeG#Lcv;t_4spI=9qQBG<ZBq>#O%4dHbDWU=yA-e2ltx1c1ogJ2?1Iu*Kno z31hfUoM^#aF3Z&-!7=*vh0ULO5AW&?{i%okryf}CL1-|=`NDFs-c5=hrj7I3_lB!I zqMwvlf!^M}A*+AOWtxxqy$k7ETkjzh8YDQfr%A5~u$RxbAOAi-{}R|j4kzDOdl)70 zJTyp&No-e09w&t?wMpiP6D7Bp$8}jh_1(dpw=e-ZFQ3oOv>d}~o=80TS101{PXkx- z;ooe&kw1Vh#OBL8|8R+}+7=`A?gPF%m*78K1>i1K4gTc+$0hhTZzS{N(0$zSu5a+? zBRMMlzeK_!fd7T7{BNM8(`?~WP`}*>G%--@lm(P zCyMy88g}@#oI&j2(aG`gRINq$f?hyycp96{C$c9^O#%ve6}o9Uf6*&Q#;x(D3tv$1>U3C=A3orFrtrq6v$@IXG^_ff5RQbxNEa%s9aCm1w@>Js zbGZnEZBukZt zt8q-pcy?F&Fh;@!proUks+BH-#YQ>0j{ z3QTq3-QioV4Y~9htA6P>Evdq!$)2vOdpW7eRctk0<|@+}anSHV9!wV?TSW3^(fje^ zr{as3ex9DkKNs?%sm@&7u_6l!It@xUPuxTY&d$J869#omVcWwo{DDR5i85Ec88q2P zX{<>D%m=7&X(jjFK2kIRplcMEJ>Ip=I*m@(^RSc~wPsrwGnALw#doRt`j=k*)US2TdRS+CkxbMkl zcAe}^=1L`uY;?7-ka~_-9oGcvdMwoI`&&)e5?gfRyh_HFuQ5dhGjfgZ{M}5GfCqVYAD*Ci}^h`cskH>Ii&Ltm*p^*pOV9qYX}SKlOOdVw1pIw>Mvx3lhHXo*gAPgWQDp6sNi zky1>x^X9wXn`2(f8>9PLp$YUHk#F!`_9Ut}eJ$sMn29W$tX+p~2~b=W1`94$GcusD}OWM3Ie%Cv(u! zsOhik=BtaXw+|6vd(`_^MYFf4Bs{G~;MXVl99tnGqrYvM4~|YeVcXM7*?rY}bFrnf z-Q8bx*Kc5LXRjbx3VB}fEGGLuFm8dl+RHLo_!s3rUC)7Ft>2-9(u?LJ1){2zK*(J|>yq6$!#;aVj%J74m4g`DZnDMfV z6`X1}qdYbq(|MzIS2&PZ^`8G{!Hd(@-Pyh!@oW>u1<%VAr-z8>^|nWGnLRm-vjSIH z7SZ41QUrG*IqS#D2HZhiMYQ;MXF;>Xw%lF+^u*0`%W3ar{auXX?G1JOHgqkK-THWP zml)W&C>PK41COyu`T5qb#ij$1&}(-m2JP2-H$MKaUX^|UxU7q3zim`=vvCz^57-rU z7|^b6T>`LO-ln|#+lObhrzYW?X3;S(GRq!Kdt2T|z!1hD(v$Z?{nhns+IySwr_V=A zd~&($?%zJ>-l%V}X~>>1lKnt_pfKZ*^^&*t^i)(rBQc46Bc0Y`f%)dW>>&RU6~$tz zy}~#4f^nXUXaI?LrkekFw22n0D?E+;WN^M-xa!k#O<`eSqo7Cg`t8Z&PbGzxboi4k>ySTpI_P5;bq;R=kfIAQ1_G&dL4^KMVkWAqvKC_h# zSFP7=wrz8)HEo{rJ&IedKVaw^OsxO#<2yPiqLrc#z);hb=aGtp`Q`yuGlmcmn&@1D zeXdN}LHW~$YrAWC_A--Da?!ss(o#RfNGQ^#qmZW+D~m|GJ`zY_7oenc9E~elosUr1 ze|-YRgvi9^u6SwFuW&9r;hAtB(LXgY(_Q08wTP2F8v}9{`m}W~7C-r{&c{W$C0_u4 zMasfIv7Y1Bk{$2PHu|Gqe*hU2MFoYvdPd5VyH>%L?%gAKC_C{dr;hZSFg)|~JA&rQ z-4pQVXQa@_ze8;jkM2|b^?)XldNl$bsrx8e=!vx%iY67!11ZSpt!D)90ry;wAMY+b zJzT^ZTx7D#R%3W@2iCJoJh9x)UqAjt3BL4%Qv+u_o6}@FS0M zb;iQco~RL2KX{boQ4kgO01J8P=L}M_iuF)5c409Izw*hmkg*U2_bqq=m7Gu=i`x*; zO5PgJ#q%TkxgH&G%?7r^mK1`P`*0ZE7mYK+FCrQLX`_`X(Tl+>ablq-Zyn(C@|HG# zJZs_c*md72yCTNxV~*9D35`WWcQ zBiRU_(nn0YGM8YuxaCxO$-aMHl>{%J0Uoc>Y~;bKd%Y4Hh<|%#KLfq*{T)$?dGbdl z@VDg1pZqhT6sXj2Z?q{zv#s_74G%CMr$?xg?+)TVW{{}ubcyST?P^4%6tf%3?`)u}BU8{Mb;KghAaSZo4 zlPcS;n{T(Clj>V}Kh5Faf58iI?~#0i-}re{HF#4*%`Ivk5-> zaf{a67aF`~UTZ09)W zmqlXUF5I4|9ecI0oe9AKPyVfo0`-Q(O-v@1(cOZe7cF^bS3+@e6#29$Lnn?jBqT4R z@_fM=mK;!y3Vwi{P|jM#BUx;-mNpAFl2DK0i?Upx^rbjGKD+=Cp{tpn^||K7bb)!o z`o(51kAr{*CM-K)mGhO9dTps6o8$Wqi;{5lRru%5xa#@T?UoTguI5LQ;fig^3QS#;VNyBOigtaKPb|pua`b?GVfq>Z ztx0BlA!jj1>Cm#ejoTUxo?RV!ng@^3DloI1Pek*hk&YGRd0zM9n6(m(Qdr-e9fe4Q z49j>f!*tqSY1);;z?y*|jhQJ-7}hJ5Ee17LHa5M35o9%x{4c`T2+tcLVGxID_Z5Fxtf<-(#QRAWHU1f03S^7)?86e zOTY)HXTjY1&~BopW1Qs%9W6+2?ZX$_e{HDZuVsdV&!q0)?8GiJnxFnP_C?y|FHcLa|+WrhPWNaYx3(M|(I- zBarKLR?<+d$#J!n{0`DJMr?oVc{H+9=Zylx-=49azn0-@T~HMlF`Q>m%16o<7Y}if zyS+q=q`~Xh{fP%n*(~Tvdw9T}nmT{|lTCeo^9#0S;37I?lR{34Wt>uPNc-!k zYqsm^faCFQx$)@i%?8(N zm~RQ4&XcbhQo%sTYj0Mf@0B5ag^tThoU?>EC{^+g2Dr#LB zC8xGhpK(8HJdi_9|71S;-udgtu#Bv`_j-`g&x@(>t;z0&| z-OjTOE5{NO%KZ1jR`Dg5C(z=P2RDTV%vvruz8ZYwfc+7_Qt!)Sp*395Q-!fGjQtFX zHIR1tVPSCSB2Cm<6! z%@^*Fc^Tto@b?ppDNSUR40XPkXe`b;^FB^LLRx~w^^$p7?$Y#lzg#9v4D{_|8rz9k z3}kQ_+?ecg+gimZB!6s-H^YHB%sPFn3n;B&Wkd>u!7tq3`L{i*W}JZX`sw4xNJh_l=p-l+A2@W{?>fOitu_pUu} zoG@Iulp8!wDRv+o-Z6AMQ(z%p9wVPkAB}aCIja8V?(}{8(!zc0o3x;KRJ~w%{7mhn zIDC2bnunoBJr-8nl1 zcgd=SYjFLcoSR=1m^3p@w0NPtu}FyWgTp=xN$1G}UO0nKW(Gw@21Pz7w!ab^VVXV$ zJ6DdeVoCR7?^RvS-i8>I6#uTtcAr2uZ4oDoe8Z73jSxezr}4WOUC&i!{U>59+R0_U z#>(T{@J>yI{Ev_b+BWkgzhw|hacz~6Y-i6duz?2{{qWF zZBt%Ik7AtYZQm4i0sOn%yDbBc1L6nR%K5F|ta&9Ya{|b@42kcBrwL!x*dRP@)8w#v zIdMGo>Ip#9yXYc(?TPwRe8Oxi{2=S?{)Xupb}Wzm#O;&v4e#)mI#Dm2wI}u} z`+|!W2@thSVU6>!FynAGn#&6lA1l1VIqLp2R0(15fq&$Nhb9P*dzf?F1~&_R!DVcb z@Fd^m*|OK9VXBZ=&rgvD-<8TNBkl=`PlA??yTHffV$m3qQRHH4j*l@)(5+X46$U`9d)-hVrWS-M{U| zRJU_|m=vM<%FT*CSwzC=ME;47hmvt-H-{TCS1#42Tskmv8S&+&Il%?}v6b9xM+V~X z3~<^XxI5hJx)w{+kI1o7ZAD`1$1+km724mvD_8xTnjAyEW|xcA+<4RPB_X*g6(zB7 zT1&MV+4C4B-C&ja3H9?l|2TosUrQxH@FryXn^mWgCgyK)7k9F4y)L*?o3+bALll(@ z$-X}KR(gaNF1WOD7geZSzrl3~;?RAXU%+VD-7&QIJsA9wp@vEI%(IxKi!QRNT z9K*R$0gTO)yHv)u&K+&>uU^}ODc9z12ftWUo#bkEI2vbU`*`ecYetKWjaoLGHCxsd z%f-60hi-7!KSl2bJ@}NXr}ak@VW8{9yr?7Q3L&iIv>18N{w1PEvpW=_w*o-l+uy^L zU@1FdU#3e!+5Bi5RI@x0gn=el#J-z(p|13dHVi0^kfIh8Kgt`kGyER-Gh(H9&pUhxpH-y_iFodM;FD1u z5A}A|%Jjgq-gmJgYYaG;wq|~z=|`c5+EuN!9;q{izp9Dem>91y;ve)~v`-|PdBrI4 zD}z9Unk(c)8ej6xXvH?n&f-BJCWp58FD!9c0NMj%FEIt^6#WWK zllb<+_dbDPo0qR)I)`cStS#=^MHm_k6<~a{tibmo26VwbdoI|OPp_fY6I?YtERYbe zV7Dv*(F2Er&lB#h+vD}BQ?$v8V}|DBN(KT#Za}GIZRlcQ;QHy1w_7cOXDHg9C0f(f zsNGv=jQS?yqX((Lmz6&$bdQF}HCCM~+mo%_LT+RUT1T$^bp;HUOKRiJA7$lt-N?tS zjv{LNw6l`EQM{?%;m;39_PqxqIFd~A^^p^ROFqckHMHJzc+thEd z5(R22vz=UO*F->Md*Ex(_DJZ0z2hWzKoFWze?z8OGh1uTad$THj*0iz60S1iBN*>5 zyJfSUw=1*SWD7)ZbY**V@xLg1>3mgvZ<+iam7#C-`^2bt{v7{4ls;&I1OAOCKz;H% zbNw>eWc2G#%A~CsfS7Z(N6c5JUTIv=b7FDkxCrKXz7)+xh(yz3yP4n9)ck5JejJf+ zrCu|R-X*UTK02YQ2>dz6ZnY)q*{@7&G<+Tzhil14lTHmL7u9CDNbaVSR`Z*WCM((a zMc1x?L$Qg>(%eEV8P{^7TSRe`(A>M8V^u@bNF4lhmEq5-!U~3)eu1_0Gi_7;F;PN| zMoTHkcLy+s2k+p1pZce-6A%q!5X<;29yU9OXJ=8KsD)}o-vEe5ve*4gw#Kw;h7#2g zy-XE4lFx(hj1E`rJwh)noZ4Dq5<&0`AX4beqT6g+^!xF7co34Wum%uMWy^Cd1$^G7d4K%Cm%x$a1hV4*y##^D0k9a4DS z%E+e4vA}j|%(N))q@qd|96=y9;#t--#dl~Icr(alUy=^Ou|f{Q+p*n%GnZw>K(D!i z^sbYhpSP2hQEjs;VcV)mc^R^U0lH?iE*AQtV1Ix~u}g0U58>@nraV{>0L?W5)_Y(=w*wMrN52apkI?}9gQ5FVh`~F zQ{UUVJ>DoiJM!D4P0LB;Z#KT%r?p)3-;cUwsJ>FXRQ9~9q^>Z()fJ)+v`oyuJ@7VK z=M{?L{(%`KJPQOfQdml|`XO`UubqgrM+J>C1`2Dj83Ls%eA8?+uKmj6%PZ>5H)dN?TGuD` zDxK+OV2b2I43W(aK6G>$AzWJ_>r_JhBE|726x(?#B29@vUiQv{(fiD7U9sMaAWqK? zAQG$%)^z6AIFp>crhf&qMZ2GV&jF(9LXMrPLZ)5ez>H{SZu0pAi^U7j;84y} zTSIAd{c&oMZr`O?#3Nl=vUtXfHLDisZ-$%7Tj<>nCkN&|h@`iP@andl_;8LJJ$qlcIoUl}MbE}=b|t5x&$dAlP* z7C#29CPTM!KayLgX5$^D^13#GfbOhy@y#UF!7dUSLOwX>baI$lZ->+#3c*qJ`LmNtx?wYi)ERLZjSIQaxMth0vw4ONK~=s8AZeoIAIv`+ic+-acHs-ltgnO1(n8kX-K41k1!&r&=rU&5Fvm+McrQ$5UJ$o+N8(ykxJ+_7 zD`h^nTdMY2sttGyV1USN=W1Llli8rUA~oQlL#G@Gag|w)`PIYGJ9^f<=;Km+R(|$r zX^m7qGZ%Ilr9pmrfAy@}1qfDB3aPg}gm#0E_=cJgolo~>=S~{T=Hzsv8X@ZEkq|pi zzO?37R_j`iL3X6#`X?%607ScT&!`i}FhK+y25*wc2asMm2TV3{we_}X`%loU*xTZ5 z6|l;eR!1Qv8ifbsYwLOwa#=Jw&&$GZ%n9;ZXb%(vgcdmku8xt`a~8+jsKkapo*k;b z5D{9%LEGcx3`G(Ao!8oT*VMJHnvCc5Stexi-^I;$dXfyj>L!E=@( zCo%-kUcN^7px3TY@etIKJ)!u;o zb?n30Jqi0(#P-0gxVxv0o#VoB|C>Y4Tc5XYky9&D8bIu{X;9?3M$c zibS#O%c!F+jgami+IO8yn|7_m%TWI*FqH1*Rx+}p)2HEFPSSU_Z=I^m1ggmn06fLl zK@d=EVm^$ez*6)MFQxF&u)Gg2W?q=8Je^7JgJm%{rZpY~#9zAB1fb|wVWQ4&O^7nH zOX^fUC&iHW4wvW3=J@CxPOWRSAZM_bTP_=GI(EFXjAoTrK3zvBkmTkV4c_g&arKI` zc=F!$!aXSZB%New^yr>_gf);!iiK`H4+O}EkH31-X#)zqFS4^ytcHypJW#DGf^7aslc|#CxV*b2M-+W1jl(;Qc z=a}sB;?eq@y>pZNl_yE`iS{#jv_A~JGMUW6XqdAY`>n^yPcD(Rct=8}v?fEhx)A5p zHzMXw&wgz~uV;bNB>s)u^Zgf~kK&~u7sd+rNo)>vK`kYdo2!}gjDR~6MqvmKhDoE% ztb%}|Gnl4-mngh?fjm@r%F^V-%E05^8J)I7VF`9s@uBWQcZ)>{)V3d&5&;!ApUlo8%P#OGN@Yh-5rZn9 z1fWD37;@ZAa{hYkg89PZ#!>GfMQA@hdO$ofDm}8dmI5WNp}_`-c&2c4cgfU0Xj?AP%aNG{$-hs0K{q+eA^yV z`hYcADxe{RF>SD+#6D*DHM(Hx2veg@G?Y7svp3Gavv~dP6IE7p%vrwnqlYqnYlu~Q zl!EFD4iCsb@BB{1F$4Ms2tzB!^A;yX%Yc&sdVxf2>mI9F;pN-1#Dsq2)D*6)xa}PV z7v1r!LA_a4G@qV)#ts^70CLtctH6JY{yYyr?r&o&gdfm8IJ8L^><+4K$REIW+D2m< zFRc9LnWc9JF^09hogJ+%Ul;sV?ZKkUSMS)1iYi)TMK|KZR4AZQH_GQHOPuZfXmf(k zPqbh9rfi)NLvB!?haLOssC$BC?S}RAS~t|pOK3NX`}VfR-+>55AxkKDx6p71o-&Zt z;6i@R@|pqlSFo^j3eNudj4Xsv}aE#nTlpPl&VS z!$7vEXr870WN(>#b}VMPG)%^cFDrZ)3}zhCFXvsRk|ScU;kQO4-4EQf!;oKke87ds z!h{9I`+L5O9#oSEtX&DD`sAF?VFH>dn|)x~ugElBU4Bm)4%?~1l0U;7pw}+uar3Xk zc4_Yx5G>MHq}rP>Z-~Mz?!d*$PY2%A?x6rr+l*#;F?%DMt+{*|yCe^q8vSd;DkoN?Z{!bJ zNgQeetEcOpLUJ{%<^Zvy${b|XS9cnuabWaaRmh))o!wlx(7sYH9QGLo796fWwpHBZ zUZ0Z+DS4wL=D4pNJ8}_wXCSPJpWNyAW9QTUZX>Hcd*Pa+8tGc{SY?8r<<3;p9b#< zCOIo#6wPSh?B6YqR`ujj-$VD6tbBrL;*T(U!uhyJ2{-gteKleXYoCI>pO)tEM zPQ6AS@eNPo-#uFf*?e%eN?TUa1NDz~0_MBpE1^tf2_YhlVzXmw7!zN6ZOeDK8WKhr zI7d2zz}J6}(H!yELm{C^&wRn#HS_Uk7V>a`Bm`aHe)jzNZ}KufKX7!THfS!m0RjdS zhS~kxZ2%2#Lh4o(yL*wo?xFkn%fxp8&6fl&={m=_{BIdjS+f{E_ zHs;@?w)>_ZEw6!rUPn^k`&bh~qdS4ucQo+3o;)rN+#HF7jw$XRD6T=Udx1s?Cd;%^ z3h&_edYl+>XVkQlZ37qFYrO}=*bq)pI;z!SVBlwkbq&*9+0k>O;rQ?Y`nK{;`h{&4 z%#>LOrqkP3*SrLk3`L@ zfTH)0li(BDyZ?(3;BSHt?)~7a@Sh!i`)~h$?Qy^N?;RvVxV>eAL&Sqa)@LNOU{=(j zUNYzDK;lze-i|dV*nE{#%Ru44-tJ!;RXtla(@jbbxd`YpFmWgjRdhp zZUKn>v9D|=X_D^qpL3k~UB(}}mKS-G&o8Du14+De;=l+f;Xe!txXy#Jkzc~FAUH0C z{5ABDw_10hkfA5a|9C5+wGJMWhkcK4I+XlnZ<`o}lA$!J3t(}BFu6r(Vj@HsM9OGr zI|ve|AuMCSwb2r%B;6uoma$QejKOfe6#HZ;@W>ZBsy1(vV!`s3NdEpL^YPgS zKOPsU(B!zEHHyy$lB`FztWyQa71<@aNh4 z4YR$@R>Aw*bx&xV)!y8zoT8N7u^z^gr6=ak2eqpA_>4i4POA!yo_+Msp)Wdauw@y zcah?_vMlct&EPCS{vrhjpULd&VwkD+jrhBhp1vBpd!Xmh`$LlzrJkXbh7&ZI)L;+-F?L3kwnhLT5TLkyt|bHitFfn2QOB6J=BSK|C(Z(edS6vJs{Y5FC$8s# z+x(?j!Mv__^Q?+SCUX=HJ~5U;2~uv2=`0(nOT(O<^z*6({D64887Y1fkAdl4_R1I@ zLda~>!dR)>?X)PI7_8EKa!S-8?v-XKSB*ALPU(-kg88_HoE#y5MWMwku!v-eTa6HW zs9YSJ_pq~lm>br>R|NfTkI=FnQutB?b$Acxq(vDRYg1D)y!Y#8sr4)=N-s3usC|M@ z?7b97Wj@Fl3r3InGU$=#r31Lc9Rv$8|BDL-vuiuq`1VO1y}(dfqrNV?jtD$zP+ryO zn4hdB50V71a1@kX5NcBNLumbWlw~iA6BI1d?s{RapFCT2mJ;PuY^ljHmGDgp!jj9Y z@xJOF%j}NWq^nd(S5n>l;@pZGuJgz-e2%l|5+e{(FWx=|qCMj0n}-XsunaPN^BvZ4 zMw8;5XSr4adIj43TLEIwxT8>smeVVoBMmu(EPY_JZ{<2~0R-mfjFs0#Gbi$1;w=_L zxnWxIYz1LO8swkTR2u4XRviOJZ;l#S9gi$k1T!+{U*8;$bB+2^&2nI3_PHPF4ADQi zmlVL3hw_E>JL{sA%OYXtFbnThDoE+97>;k^jNY(Lq-7vYZBF*67?%9gCl7w22fKaQRK|v$1%B(CLK^fF7BmN0tns8%;KVK1GnN8ParQK+ z83(_DhN5PH_OP*&u8ug&?Pz*lj=$oP%%VnNJ&F+Cgjy0I9qxtxKn$BI^jX!A+ z)4Bl)(3h`Lcp4*pS2;I5xwmxms*cL)hN_@y3G`YKG4-JYo9D@<8_p{3k(p#77p4e% z@fhmSQlePBZ=wM$N!)&yDnuL-EQM550$4#QjAXfG3Qh1mC8ZwKqINns zRwZ}Y!>n8m-LV!VrOmHhDl4=0qLBpQPd-9Pv;dJLx^1ze7f3QNK&IL{vA{*yT=Hol zVy0Q5{xr$r%d^pGFe3od#pe``?y?xDvCXtQb(a|+k1>XnyH_~FiglsBaUft>bL)V# zL4C!{9aobFW<#hSSX<@c45X2t5qrORS^8|TG1*Bv*^?fRv}TW|U$w;O@o>4o;Ud2Q za7lwVfrXr+>5Pj3883lX$iGq>RGoOU`!HW2K%#%Y^d7|Ax1UOHtRs?Y=|Ei^W3qX$ zxtq(TMWeywxmX^#fsm!6^PEVPw?(}a-OWi zL*ix6dX~vHHO}y=f4l^a5K4uf4bS=PU+Ld!z{=(OAYd8~^bZuz8znO-PiK|LljLZy zVVt9w*cCe4l6OxI^wC*w1(Z=b{klH<)TmE9WKQ|aKy37a6zV=n*&ZgVvGZe zocfi3T`M*YRwyzqhyt%fX2+9s2H#Y?uWPA%G3rMHEM#@F*?}WNkwsmw>*sVrr5|nQ zu?kr(ml$2&<-KL*;#Zl`McYs=iSiy0+wB&U7K0`j3XbU8FoU+)!wY;nC?*?VxE{{2 z0_wP0)UvVe)!Zhao{;Yq<0q@GOkXe`z{R6V&IRNhUq3${FCx_0uJZ+VfJ!nH=a(%qjJZ0WdBJtt%{!K^`l zzb**a_Jz}x6D1#Nqg>e2FW?S9P{D|ICQa>``4wqf1Fd?=#7NyCK`Sj2Sctn2M03?3b&FVNiyWBrPR zZsC*S>8z;Lr0QbE5nLJw!I1lD414dLgkbAY^O+`j*22h=R6GRaT47}~{xKGUe^M5Q zf5S%&z!KvXU^2fq2H{#K`fs`D_pfgQ{@uO!<$;nOXgqozZ<|*W=?mgM_y{F9blgO; z4$-mVS6WvDPV@*tn+)`Os(+95JyYrPV`&i9vG*(M`cLTiPcS5*6d=+beGhvtTpV8% zSfr8oFa@YmQUM)=Qsv)mi~e@pgH&luw(n#Z@|vp!cjvtmo*y;h?xbpFYC~9*F;tFy z$UjusSUp__mHcQcw!dHY08>)r#T%9J5e>}7Ba_PEzHIy{pCsE9IyLoH6u|;DUbHJc_D4 zbZ{I_Di4G_4XoQeFYCQ%*$5K@<~M|3(P?ud6_R+GZZ zn==C<9}t-8y$XNmy6R{dE_np|n#?-)9wbx4tPewjYx0*ch&PHwzl3<}ZF0c@C#)M~GQavwW;~KkU zmBZVxVPN=Nh8&X6CdJ*0?Tf!)y^#er)kdPRdH_U=f+RG3o;9t0Q!B$=Rp z1e9i`3}RUyUtL$aeE0%~Z^uA64K(Uynw3as6SvAU@-fah&1o!?W6`SRIr>Fa^3+~N z1SS-7Yyl5Q;AVl%Zq51v!1LcI~@R8_53?n zYV<`bYUOm#4!=2agc9lpm(k;sSDD>EQX2K|4!pB9ldh|H`RnEV1^Um8)0ld@Qfl?) zK?ZeYV)2X6Ff?`O=n(6~N6@!u#;lL20swiS(wij*#19QQB%v!4JM4J0wvPf zXjw|=jafye{E{Lg2PO&=fIKcpsSDzy&&4c~U{|9udj*YB84onh;x}-zcf~Ju$HTM} zNLhIW7|yv&NyEF8G!2O@xrk+=sJTGBehy04{kJR4&3GCQCS>*&q_~Y!Z{a1xE5bAr zJ%}}~>{V7hx!pL%r_D??TKatWH#=X|6s2aMJ?{d-G~tPFwG_o`1KHK5!dCHd5}|jA zmh-GCM|sLlmS2F$X4mQp-w!xFnEt;J$Is@ks%*wBUE$!i1KL$VPnO-k4Y0@=(H9T) zH7C~~l3Rkt*L+Fo77eI&YGlji57We)sYf5)_<5lS#`}pearL7r{Or9=Eo7{ZT0EY2 z21HwZY6xHni#s`Yq^+7ccS?VQ!Tx!EUJxB{3rYhZI^0h>MB&N~AHLCvFvhK$Jxehu zoq{JexT21>tq0F%qZTEl{v%Q8^-kGTty8@ua6K*vf3OxQ_`eViR+T&De}N1M2!G(q zM|3%-~DUG?ABS5P+9mnZC7(J)MYLP&IofgA*z7=6 zD{NtuJ4H({(qO0$sWOJt8!6Q)o?rRmibAF&VYYf98pI6@VxYYi6vpg1#wFm(-*W*B z)axTJIiD|^&zb21tmt+HIISTm;Oe6 z{~AO%3&!7pP?0)6P0iwcDBZj@uiLb$X)E5|%Kv~>$b$Y+KBTW01!L*%^t#uN-f;XzrKmSr9q^mW%LUNw?y?e zOM_>znv(Nsg~uLK&~(p@?Leaaq(UKHYtnne?vfp2h578x35x;HC$LfYkz8jX6MtUJ z<`93UyB}{7mA5203V2F$>pw9bh9RtqYWW+Z-9WjaXF-Z;W+$X@JlHeTJPFD=tK6iitxW*T}| zi&DHZB6%$ltq*jrnV*SPe+d9-L1T|B`C@ho}RvGg{k!V_6Zl) zDs%$RiJ`~B50bJK{euGIJ1n&l_pFLW@B}>zJ5cX zH3lYSz%i&wPR2d+Vnj|ig~iIh!b2%SS=uT&pWn7)#{84#Fh`wKr;qA5^CMr-f&)tK zjiAxox3PmQtzcPEk~mXMTCrazZT9f-%{1+)(UJ;i-4{;q2L3nZ+FK_NB+(Yl0-#fG zD#D;L1`!R6P&O%i`ng=o~j>W9Ul=S6tL~{}#du+GbEO z7g+tn&{Dl4rnF!^;wU3R&|5qUF0@^jX;KV&Ed524w>lP4aH|%1GKc%xUjda_xKnFs zm|V%p^DX0HhS7@VF5hg{ucZayAsPP-Z1rEch7BA38qg`fbN(?>EOXmK0mDN)ak?>~ znT%^7?+}GfDQ+8}xO2Kr(8P^^POxnK0u`ZOw+r*+*Ty?FO~KH10%TM^M9p{K-Q=UJ!m?bFG&EcxkwaVL48?mm9lj`}u7j_Z-=sXLYU?JrB zH1UYL4;ZxkUIGDp0sxdeJ)}1;w&+3o?tt}pz0!RwK9MB$E{exVG}bdI>UR)m8{!RM z@Ff`7SpK0Be5JX}N^?2U>}mDMwE!)1(A~7S{~rjou?$blM#GekYiXNENMdV_W#^izd!UEB6fF9ZqQgf5aRH4|y6K14vU;GKS?@^>U zHC-}VvDytLfm;h}3(Lv1dd2fBKBQxj<;3g&4~L5g`;{KwOPM3GdJqAFTvpVxp0p7r zW`vUosSyyWKa;(99)z?`{4zpmCn>PL3X6#IWaw#Qt#i_FUx|s@sn&{-p0fyHnbG*QIwr5=%6FS1|5W* z0oRYDyzmh^F)U6{99z9zR6`t1@Oo%?fRK(Hp{KopjmETu$2_2FAC4AZ7*$ z?b8-JBo1pHce)UT|Aaela{n6>qx#FNc;DjOBnzGA~?ov1>{ZiZHz>3J&) zxJzi3eO?plMad>1KK0eNEHPJW1f;Vb(%ip}T0it22Bgc0i z73E*$$16Oo7xaRH;-Jy99ir@#mz568=2^XpNwjKH;1!AI<0a$juRDEqi(WcLd^}w| zbLw?+OH}*H64Z2WJcp}4yd2c01qr6Fz<*(wl;16&f>YUd^o+l_?8giEPoIciSRm+b zy$u1c&5lYQKdy zn|@2B_o%qe@~_Sa{3A3vN5VOLlA!vET!gg1E46Z2wNjY@mToQ@*n?i@y;*t)p-iD~ z##HtPP}BnX9Bp3Jrzx{J9_2=F7_c*_nAaVqBdQWX`>&j?&)GH^^?BZ3k_&GNLH)1f zD9KwRv!%hla_Qvmm6Ij&yT|VFuk(9(qC>u%Qm7(lN;2qgSRXr{O!FDgh_l;|t^zBP zK&r-D`0B=f4Wu7gCJc#qt*l1HkH@oyumvQg`j0Q+Oj-==bzMN=3O&w&7;dI5K4g1^2UyGjXf$yJvDu)PLC!4Mq+`g>Rk@=O- zdkTsV#ToxnRV#VgcilUG?3oy$me}69;+VyFrY>N2xkrcvL52$o@u6ybb!miq( zjh6oduKfr~HGZkKd#*)~s^lGhZ~gqum4TqS#`Lwlk*Stg8E8^W0~XBGWRX?GgK$*- zUj)3f9v9WMnFj1}ZCA_2)j7wV_$IIp00j4Os*7?N7(D!HS0(cf7g?VJ1u`5!s%=hS zG7Q5xRJx__gw%_h;GgR36>{;%(nvB`AqXjr{FI%^H3S<#8{Afz%hSoh7&dzqdcrup z2A9F|#VajAe{u1XO*(RKGO`(uI65qaB$GlkG{T*y8@Z(e3GBktP5I*gjZivz`4;ck zw+MWAKG?m$qNUX9%1PKf0>D)-hagiHT1WYk4A~$Xw!?aoK01LcGy*p8H>ML9D zk%tH7v&0>NQtV&c1Rbw?WsXAayt8y1fmkVe5}{H^W{Q<9Kwxsi%w01WExl0H;Qa?3 z%G%TeO(64if5~7ZxuL#a!{xrEvc9XQFao*oHlEN-lOj1ccasEV>Eh%f4MKzZv{-Dw zEs^u>fdYi9*LEq^*aVkrfgH3}MM~u{Rk8O}pm&xTd3ES0sI%C<`c}Ne!-&6b?DWte`EK`-UzEx<9*_ zZDUx!U9EvyYbq%V#2}Q%YwurKDLO37#QHT=w6ueB`E!ZfX)+Vn{@Bu2K60 zwpjur@kVh{o#;lu(baWOTSuZ$Ykoele3{^6_}uu<%>h*<05kYu4oG>S{_2}9)^L&p zEenl0)ZXEk=t84N+rd?JCC4Tf2(uihAc+0OQwzf0NeE9OAL%in-sNwh3Q4GL@!h|H z9zbr+?nDGMU6$1LEMuxe9An*?80>#aKkl{tKU!MsbPib1B|6I54peyh=vyy1B^GP- zKFu%ymfJl#j#XHT=n1%M_IKG~n_8$Dik&>n|H3m5c#XdtAms$~R#==#+uU4%HSFL? zf#p+Rl`J!RoADY&X_sTCO)!Nc7?LJc^~?RuFN)R^5`P3u22G`6VMACF>&o}KF`4T{ z9)B1s1V&Q)`fLy!L&MV>jhGQY6P#)5vHXUYL6eoEwNjBfMGn8KlqfuxTe~Pc?yOvT z^k`K96PV|KF_K7a2l*N9L8vrwCO8*iyWmv+-?S_fS=c5N)1snm-2PE{*Q9yQTd^Ex zk>A-G7=OQ`|A8X^6N&6*jtbn8Sj=4oGIp|@jU=;780*T?xOX|Vp!i2-zdb z&)2_nE@&BSg z70J1bdCunTfF>aOrk{482$HVuV|l;C+jS`R~MWJ z$<4|NxkE`^AG;j~uLKo~3L$GoKJp?EiTKQCxYtY(1fL07IzV@K$E(U(B8RZeklBa9 zn=|hMl_aH zzVJb&Nh)n5wZ ztJae}th)a%Y=8tAAOWeHZEZZqm1px{K^17`_1@nLF4Rf5a4Z3fL24lC&OuKF)%2IR zzkp7lyKC77TiZOr9sySZv0Dj8S&dYn-)4CyE(<1En3Z0-HypB?$q5#yEE|VhDZl=` zaL2zhNc+P$cZ~nrN%WVx;k*1x-7p2CF|mD%b04@&u8gxXrgh?mReLn&>z-X#hCB}~ zNGq=Y7yZ)-teG!Ft=GGcylp2Mu1D?{-c8#Qd;Xv3M#L=rH;%{y*IB!7@~CiTp90dm zGnmpr-D^4is5&0hSP0n}Ay=A4FSJ#pI?1P;?Jg;zrBh5H>+Ug7?-tq@VHnvb{e=L}tL({gxTc+sKtg{c}qWl-J8RN|UF%uYIdg#*Pux$0v4w~V2 zQW<(7+INA^AG({d3oCZEO&`s8`=l45nYKmqo5@$9v zcBy~Som5|Ie^@rOYo)TnlGaGZ(hn|{@Vl93gd)EQ@zjWOXsI-4?t1{5!ReAs-zS?l zBr`r4pjPK@f6aFyohY+(Iy8zN$ZlCPo3g0J18jfbrr=i0K?mRa>SvY!XHl%$5MSLJ zp-X3Pp1(OHn7?c~>}rv(GS#$j(JVkStwGf+9_@%6NiWlLG;O?vSy;PPX4k9J*{VJE z&~`w_tbC^ZSX%8}0FK);T72n!KPQVqG`NGP(jwJ{TGG~w+X}_1!0CuzV`fv;{V{lK zqd;AZZLf}>R%OpRE7D*P*6g?qU#=7#O$6>LB7eby@fd-yM($X?@7Ak_BL=zivT^Gk z9=dkf4Bi*cDrNyH(u4NJw;k+X!*bkLHJ#i43K`$~6|v(G=SPJPu1OO+JG!?!iWQlq zC~#$y9T?|LUnGwfF2eYV6KSxd;eqDDhqny>yDbf!jmckQsuCoS@>!=2PA*~9`=lp{1uKcjzh${?W60N@e z1)E$IKhH z3no$hWT5*>mmYgEK8cIMayfYMG36W$9HAsruW^IcYpuQ&A{@{D%%1vc)dW)YSfeND zhb_-Tvzr%kB`+p+OIZOk|F%K}q&PLQCs9g~N>snErt*kyI3oFM@|0c~+xC1;&~wz< zZKP}H4IjSqB^qiq5zrnM0O*R$ewB3e!`tv;V-9g^;&k;+UqIj+i zEWW+;&={iSzp1f2_ErNc$jr+d(Mr!K-kaJ@ztZy8KXjj(`4mYR}O$FEE zU zjpawjvyV$#xfA32*NcsJdJKUHUQ28gupu}(V!R|v$FD+(bZ{>tD1}?C%re&pd9S8= zd!U$|+a97^&aFk72I<=Y*#N_r^4elU$PhSvz4Ui-DNP<-*ad#2a3r z?DV5`JCNuH%R>#-)wZI{Oy1d~)M}a_{K!{$B|@3$ahOP%se*k{C7P?l+C+weH=H$t zxw*abYhH@8TU$H~$tvmoCU!W4)So!To%Bs=h5d0Zx^!_20Z>~Ro(c)8)iOUQa`5e5 zd$vRU5m-Joz1%eS;STXhML?XoNblFgkTc6)LVwHy76RDIl570HmOkOwWIdnN1y_(P z1@HE98SOFaTjgF+@&aZaAXz}l4*o|MqOZRjzvYNLZ9utKo0=#dWvM((1Qc33ecl@b z>D94&=bKJ0hd>q?pnbs8^rT29H<7z)eKT3mCIUZ!aKprp-x&{w^Q^OqA=k(+V8HSGw2HM$zT*(oPB6FC$TlQVvUzmqDBL z++RP$6&8dkv{%)(;o#I(q(QbePIuTKbS`8?JLA11R)f@@PJiH;nn>r2*4@P|U>au8 z^XGMlWozFRcYpj2&q-F%e`OF)-sCOtMHR>If;Mo6nYSsPupB{{*f1gbkVY<*Y}nZ(o%o9LIl-mv&w z5JXdvA}8TFmfs@+0L>lotSDRyy5Y;l`Lye5x_i%{jgT`>9Yh!Ro-XfTU^*5_p-A`z zbqo5&?FL_~`;_wz`jmX=Lmn%p=0kD4qYdwK zYdoF@rV?kT;B7dg_9w zBE}9WzFr9gWjQCLh*>mSH8gOkqDTDp?`@bF8*(}^vc)YD-E$A|#$0Dyd>E3#84FxJKq zV`Hn$e4WmCVp-5XYHd=mTqpPQ<)FdyI1CDg`sjHpZ(hRp%`qy9nTfLMn{TD_SOwE4=AcxId(Uush$V>tK z2G29Av!2878KOy9JV4k~5M+01G1o?(T(1v!BYl6Nd8<=R3s=}47$to1}2F?_4&PwV?SPK&x<`oO(>+g6w!bN%FCFR(AtUBpWh z{&hj`Svms#SkWJjLp=crL0xcQ5hYwatc2EMbzHUz^?ABm{wUrH?Bnm0nTLZCGMpG1 z&pNDAXVYY23d|6#$8!Y3La&M@hbjxtTX?;HFdzbdUe||mSlH{5q^V~p{1j=rUtW#pC$D!xg7~DmSvkEi`&QPw zzR6m2+z8WS3)7tJSa)*?+^tItFLFxu)Xct!VZCtBazo^bQVt14cLo=<;Z})imXQY3 zSH(UG)cr^kyA{S6&D3;@7F);KTDlo$696BWe*vCODnl&x*3VvaCB#nMTA`PpvoidJ z5hly8d8G_kJA~}yLtD{E7~61&@qoCSQ>r1=S@Qgaa%p==9z4}`A=U%qqhcfyLTkF zIhU^;!W*|yZE?-xYF53Cx{&*$KU)bg$jcg+z;ITef(b4&`YuPzS2exD7qZW*Ck~4Y zQYP7`)<=s|+))}wdjKC{;XZ!L??hEeoDrI)$eH?oUHi$RZHCTq-A~?Br(|(P)6H_k z-$I2ddsS&qyX0E_O;L+021jpRzd)H&)(jEarId0Bz8Y$(SmV?6g)}`!qiy3lon+oj z8NY9H;h`y}CeqU|MjSMqVI)|EoUaUZPl1q%ldv#dY)eS&JQbgcQOVdtBIVe3vM$-m z&Fe{a&rs+p!!W_++#lyVaY7?WiYH4ZNJ->gyi6DfSuzm~w8_{P@vY|0H*{#~mp(xB zO-yPx;xIu}zAdwT`xy#jVX14%Kb#xeO-fEqws|KAu^eNbztUW@h9;QkYXVaADx?Kd zgp7`+o}yiF&|Vb`&>h>)5XxQWkvCF*qb368-3CTcj$)P4-wz4zYd8L+O>S8eG+S#s zd{RGW3quLfA79@RXm+DJUVO6OC>dV~L|kF|kgXX^%x4#9sITkI|1SUm^8*9Ce-vO} zQc5J;i_O~DiA%^^Oxp-?*1wM0F&4pst?Um%J#fda(TpshfkhhS{~#NT_QfY&#}e(|$WUVK&uRdC0tOrFG3!pwE-E!h=rHx?F%8==tLbYmqhd41%oAws?^JxgIGpmDxtXn< zSb3D<)61YFlr;}-WhoqS5N##M(2}J45{_i!FiLl9aXBrm*>phaf=pH-T|F*N+-g}Y z@Xdf6>t=}R2p?ZTzAa!bxVQR}#(84El5^OhK%lQ`WXfu)|k%S@e z#61<^rbW+?!4d_|Wdrl(KDDv?PSu_L)!A>`?h)k*mA^OUrtGL&Mt#IaMN9pPupD65 z%uMltL21}AtP|)Ql=l!uHnyqRk$9Rg)@FWu;;(DwL-$rPu4qua_c(*u3^O3^_>{F{ z8;L9o556CGEkjYb>r-dpC)y~#rQ`XANiy@f=V0jr$U4Z?U?L(vu1cF-T(J~zI#v&^ z$Z--GFtU8@NE5ztgL%Xf-*~e5ofL@wBm77F52A&C4Pt-4*Vn&GdBnZIfT_yddN-j= zdeNSyqizW;+@JgT)6H^DXE{-NpSgXXFO%=&nsn}?-y>8ZpiO-}0xSn+;(@bpn5vD9s`Vf(aVMNmd3gmoj9JaY*J3#-CRKOXNTl{f;O6iJh+q|0?>k3@un9{e*@qbV{`p z<*s{WlbZYCBeou%B?c+*60~6adU!mXb2fsmZGG)i%>1QA*WEUAKLZftTrU`Jw&>kB zR4n1Hdb=mleV%>1j`lkjhh7>(kGkD(LimhF>?tz)I6IFZv-Nbyu3>$Vm|&_u{8UAg ze(_`9Q6+F1hH@Gq^)QIk(wpR-A>*)~Cg@J5`SO;NoB15OUia-J~Ppu`4TZmY)WDqRQEcMq;pFr*4qHMYg}F7tJh#~m4tc@bMX z29}A&93cfYk|2nX#<8$j?WBJ2RZ*^P4pUmR{n)+zIcMf(*Uv?fQOiDkQ$kX@@jCrt zi81_J!xK6ki?yWiqYw`d(a%rhdL_WS1HOJ~gS;6XytbdIqOd^ zU7lAQ8aLB5P7~zlVCfJk-Gi#zA|j)R^4iAQM2Okij2x69WvE+8sJQXIwK<#Wg}Sh0 ztgqmAot7C+!6G9T$r%lG)jbf=X0n_u%iPy8wdSN?Jlf8x0rStKTjx-v0&R}o{&K>& zI;2jM2UXtVP&BZ2e+-QrHM|DVP&J?e~^R6)u?nF8IR3@G6Gt2bRYU1kldXgN&p$m_7HeJu}X~W z!Z5L{50Rcy!arETyf#JmhU{JDSULmzq7dANsFUiRd#E}Hv!>-Tumo3sZ6pQ^L?*0{ zoMdth&Gf>=4uLq^_IP`zUX3&Qz~B0x_VC(0kQMo#coReT(M;5~lf9xuxTB2kG#qEt z67#_deIp9NG}Uy_+#*xT($r*w_rpw)OXI?=mQrvvtVB3=&WSYE4CT)rOp9!F_yUkS zkX7}P!K@UXMWM# zw*Lu7-P0n=KKNL3ej`+En~RF=lumZUasfOKFMv{MjbUP3WrJrAb5nr5xNUmRPM!qIO!Eq1mH+rcy4k#-u+mHWP`6W47^-GndZiePu{wU!bAqQFzgJ zNk0RLVnDD-OoXaNEsI2JrkB(o>blmF6YFb`-iWr$mF^;$cmSFHx&?Fz{sB`@H|$9o zAwu*~$iK3S%~{hD3u!-aW_MQ1Lh@(qZQWCC4TicJygi!JOFGZ)BZG&#Ep7n3{tkkC zJcHq(x+_mO&ca1$kdS+Q!=Jc!bFzS%Cp~Wg51pm`8d~O9bu5c;C%L>4+5;MsaIGP! z3Q-C`2AeP8rOSS(45|U2$HA3OvLTw~Z!(R9O57pE=!mi8o-^GJ6%}Cugp2}|2jkXA z-ym!4s7Lb@3I?EOm+n`UIoBQlfOJiw_^!KJ8TlX!en$_62N9ZD*=CSQ&;$V~vtnLP z$H6?fM$IxZ!}SS)T12w$I;i*ow~`;qqLsI3c$>`gi1h)*lnM~3h8}wu_agOso%jaE zTRTRhXjM7fyG;B%uGTB`!fo@-&w=DRp`e1TUXkiOf5t}_=ueh)8Yb9I9;wY_F;Z=n z80tl5xLjxuv_nNURW#9mN!c7M;IW{`_M%mJ330C!BarT6-%B6BA6^gKA|9u-K39;? zro?zOU0nyx3lxdgu#P**ZMW@?a+5hr@Zz84g=yvSpn;s-@0HUqZoqDTs8H7XNpi!X zM6TO@(EPBdRn2!3AwT0)Xl}J^2YD5~Idi>gf3mQ;rA+f^LokFz1KR*Jt;}KZ#)|2*MsKaf3{A5lO z4yd2f2R`3`sG{N6TO%u6pl5(HB;Ni#Gy&*IqD%Uy2OqAq%Bmy?N_`QxdLZ!N)fkP> z?33`~i|-qg@o^egMV3JO&pNB7NNoJ% zwpipDwrOo#YhV1bg} z!G{QDH<4Z|U!KMbqvBUxSJ^V1DIP{c-&}@tihUR2M%RjsJ?(Vn6QholwhDA`Z?WYc z&{D-BTtAvy-&oepc9baN{oFPAX~Wy3c!swzp@2z$eTXR{Re-&0=4b)?MCP_Z!pnqC zn5t%?*H%v+59dRp&`Hd>P(3_*OJdhl*BsdruifajgGO5|>WOUsQrTG*rbo6(vxKE6;NYo_KO z0?ksMp4(SL`uV<@rDGp`AK+s~Nq?gwwc3Po24x?Ikx|3{xKa&HK@!+n>;A zgvq=2MHG}-)^x6te*91ZY?jf_r@F`0s9;QWd4)zMo(G|e(D8hs(WA0YAq+R3*~wyZ z7X(`ZEL_|5PAVtzv5X61ie%JqNw-KZ0`Un>ChLoMM@1g~mVO2FKMn=F)Ps7Oup z=(F3#CP;T}xa6fye@$okL--3P+pJYp%Zkt)b8IR$PVCaYb#7CEl)m?BP5r)D^byN{ zBMMUqxnW?Rbu!n!|@)iRsID=zK>z$vt`?@HF}}v!vUW{bAj`a zc8~PN_sKEUEa`xB$eRghQ1N5z_Gj65w_nq4l*8j=pHQ;YgQg^-P|PM( z8aV0{lqzK)jc0gBDOC~~{1=3Ssh5GSw3wUVQ!nRNuBq9)(b@_Q=i_YZ?TaqDc>xt_ z3~gWe=5s^>S}CA=hRVkwFN9@RMsY9L$T_gVF?5}tJXEP!mScZkXu^%EiWjVT!yMGg z7*VUHzu7OUlAr&xz0I*I=wTza4eO3@l>S3OyCDgT;1_>{=#Ac(Spw!G2Ov z-D^3gtb`pSxy<8~jp9wW#~w+^VF6T&U~KGl9OUA;cRwUU^&+-fY!u1k6~056W!o}4 zLO}K+l3CFb3f1B+T3~Qg;a@nl7h!+hQXwDyN; zG`jIkP@{nujeXpSPgF0`?7#sB#{Eyx=@PV^Gyb3r2x4zO?HRAIT!lQ{*94Q+yb=ro ztHpNY>CB=ZXHmdHLHFYtbCX9Qe$~=6zpxQHorIdy_MY$xk#mRAm%3(ZH`}*5*H5m11~&%c+5OAcWL6SK~k2CDXt2 zZe%CRYFxOi?9pxug01Cf*T03V)b#NOrml;R^{F2|J{(en^iiOv3OO0b2rM!!*NbL! zSH=RNxbyxcPMQ)srZC4`$NR#V8fvG@(6P`;zbb9`f|cMQGW3G5Iv+I9ao z{jrLE=he?v5hpydMog)z;wHaOMao1(8uJU9=)S3y31JSdj_5pi56NAzJdMMV=2+rS zfHxNXRCmD9{wd=)g*9egXa_xTH5cc=Z(Et+rXH^I%(&*=H(_;9*Q4M=Y67t!?iyx- zEX#q9d&tUUFWvA14ju+v2;j4cK*L%ay!=2v(Kxc8JFDmo+_vzrrmrF4?W?9H#N7XJ zrV?YkH9h(P+P0hv>Bg|>+{6ZdeN0keO@{PHL;JbZ@E~Rv=e>0EQa+>}~%}v0pQ9B80SCCAl;Abkf z92!PT;jy+C&9I_nGH@*4G{d@oLGjY)2~B&U77Gkyf2=gb;e3$NV>bH|Nsz<9c9qs? zh|$WPJ7@$!*z0KGqu(eU*8vlc8@7(UZcd6ot_p8G%M@*;lFCrB8CWX>qY{635k_VWpavAE=!M%}jL;8h6gn7)RlO*>P6hw$EuAehK4E&;4j*SY94)m({&M$d-O4 z0pi_Z#0&%}k{98X#%FhMnPs1fy5#Vn?*r$s83&iU4?#;jmD25uTH$ zVfI{n_IkZBIgadUw8&47quR`u>nujQ1Ab-p zL(LLZQZSt5@Zx0`Rk6RO%R#nX7IQhwB0Fn-x1;vkCTi~603|gH9=*5WJ^IAd20$Hc zmRfjXX7i|cbHoT46pRS$n-kjWA_yQ*Vi>n9Xc!gSD!_N9~wf%&@DK4HN zP)$wK5-T#h<+;qSurQ#ZTpi|83FeI~PfcEEZi#Z-It|l)RB2i$d7O@8yX*vwW2_x_dNx~Y6^h8Y7s#vJ-byETXW)GhZIT@cKhvh z=;L7G7r5(L$0UUggE9zN3~0*8=imvEC&`SkYkZuESeK&x(*(r_k&ijPU^z}t?_rx#r;8>?${h>LPq11o+m25m^9#yN~8B% zsWS|Db+}%)`jZSBegvNHIO}k+=Cv!8?P7YnJ4`EFKZF__yH^qMhJiJn7~k^2MLfye zAP33SwVobGfPwy2&?dD~a0X%s=1g*M7JbGYCXY5;r6-+PqP$?811I5tFWMkHtf;i* zWYb2@L*>zt3?LpL!xEA-KNL25+B>>G^O~Ym1 z|2I3ldGfdm24Dzj2+G~P-MaDRhtQI|!JPCAN9Z`Btk!-Wg zvN`voi~oA&zKt}I&N)V{@G2```12J*KcRuR0Ny2{1?mD zA+z~{Xe%d6T6d7n5~6Tl*0d%dNA?1Lz=N8E79aGhmEuK|nr1p*pPC1dVJ&$F${adp zt9NCQ<+7!kU?+|pCN9iT8iGi3eORJ;iTKQb=`S4E;aVlKr)Whbe2>y`v32{z!&r~U z6143hPg!YWraYH(&w56R@@Hf_adSoso)A3`4pU^Q0ltP?OtSBYKqD9bNxQ{|z~5{7 z&6$l7QwkS@wU7kA?zJ7uoY~zUrS$%bH~y40ThgO2e7$v(`Zk7uu1l7jzj)zKegEWz z$7Y%ffYSrDB9I>1s|7r*3>D4}lff0y>Qz^3rl3%k<{z9_fj0*%dibMC1QuAftXBzT zEy|rtOCTeNM2{Ra!7V2CW;}1o)YeCBD-YcEt=8i5v;9?H1&t4)jU^^IL$}7DZ%W53 zPKKZjAKc^Jnlsj+uKJSZfhXbYe+a+-v6M;w6^g>;;MxIA%~fNa&z8 zGpf9{QzZBTTXeJH&Xf!8|HPkwzNsIMQE%b^o`bQn1|PHZZ(Sf)hN3qm#Hi?kg{JM|)vLYjPJa;G(K?3Bk6q&w^QtkiZs z${V{0wj+T!r^GNrp8C~g&cm3Z_x?KkNZwvm(9dsSF3>WETAIR&x0b=+Q3wXFK@4YD zkEAL0KPWX%FHD~Npt;Q*tnO(xP@djKDm-vkw{Z<-pPWNgb11dr0k$IatiYcl%E~+0 zIAqX$4s0O1-%7*sRI51LJ!zIdTaz?kwyni-K1Ns#Cap&gG37ad(zW@{22?5{gnA)t^G^%ji|hmz@K>wdLdt4wpNxZZ7%STub4+`4u;VW}aO;C#+&W;0<+(g{pIb*Y*Rytn}kklWTaA324QjLCK=RXudRIU6+FSVVE`Tc&uAT$BRVNWmBAyS zN3A|Fd|CULyB^Yxtmb6aOO>Pj;MKq4ViX-jwX5u{>V6I4-U@)K6d)DJ)qn{SG|^1+ z5D@NrhP(j~r&ZTh6usJ9V1Hk-`S=jK2u4lb<8bhUN8yWB6&^a%?;O&tE%eLa22?jd z9WlEEcUUe zXxla8dM6yK|salu5=Q2t;>8ou!+e-!|OFPde*|MiKEi0N> z{U*ze&zytoK9y`*TY1UFRxjEvyv84Q?mN1`i|ZHL&?TqB`G=S9ZiSNkIy^z(3o1>V842NgfIA76>)G!&C!1%eXt(gI0*jaA36NUKox`-ZXm zkVc)}BkQQ!8lkfb-niu4z&E+q2vIF7Xj&EwP}2VGby;yOk@ykB4tbn7dshT3x1$i{ z(+>4;%fF)Rb=hz_QgYUKv1#?}?EAGbIOW^0j_Xh3<4{1$eb!rRyeM1d+8RoBC7SNM z6E7o3ONS@XJ*e-hoxe?nz<*qZs~2Z*A)nR`qF`sPpuRf@KU}^z^QqFz?=`9cB3p1C zLK(iP!)eLPi1($AUht4KA0icnlzOt5jAj?gZv4tAVD(vNiqTx}&r$sVo=s$Q3kKh*vcA0EYYx;w9 zoRtNW1F#I6btinfi)%4A()ZkpReuwYTwn8ez}x)%7(RghwciE@DM9Htl+>L$5Mgk5 znM3A?5x&gjE}ej#tQ-OK>HuQYmnA)H^MF8XEnD%}D?u1qFsTEy;!x@G*7V7-7SzrN zW`6o*R)mou)P=;zn0!ps@0myd4Gvq^DOnBmKkr-oqXvPRQ0y(3m~?Ro?xjGJ)|Tnrb_wUQS}cl$5b ziXSyDfjIPq@(%f$QFC>VLDVg~Hu_x48RzY1ho)P}w}w*ed!-BEsE94Ytjooej9v2% zml%Z&KPZj_02HBtekA(OyO%JY@JO34Rj@%APa||JWfR-s2^^HHnAOJerml~__Q`RQ z=1BfxYUnMD5L($T@^nW5KiHAmoChJ2zdHOc={NX(TmN)AAub9A>mwg{c|d=dnaVS3 zy5X2}4IW>4_u7v@(y%#->HwZFjbq{2lf=JqjTD3(vWqgw)OR*O)$F}LBg|d&)sW|Z zqhy2j+lT})Bg;WgfP~nFi=yedbv7spUlE*mZYr3bo5b30N6%rC%?Lwwe}Q7a2j?d$ zwl~^alYtD5f+X(XWEz25M7St}(~-qcVKx1=X!7P98tX`^-u)Pi9nCion^0oMmg;dj zDbqNNqYcHu2Jbl|JzgRr{VGC$iNSb1ub^jbSm0{mEkWZ0$7yI-q^=iPcK6zjO?KsC zjU(}@(HYhara-$hxW)oYo*tdyvs+))8xVGTW4u(Igb$W%V5$1fj9msAf@N-solAS? zER$t6DdU$Pb zO{RqB<{8RNbm$ZljaN)k1&tGO#%5ko0&&J;k#k^v5YTN~lHz~m5+1fi2??N>q5ahB zix_WXKL~9FgDfC2cJ5o)n&;-vijzH`^P*93Vrg}do@YJAlhxCBdSP}`1o z4XrZMiF&v!pa~G(w>6{!2dSL~`*@`MG$! zxkx48QuBvYCl{a=;oa8gYZ^F)ANFLJ`amkHQ4bYa!u6vrB^A5P_?Ee9;d~1*e5ogB zo-ER+oZ3dM9oIk{r0EaJGUgpdXiRMpWCfjaq$-aN_A&;ZWrE_g;+4M%7oPuZjzqjk zL8ziT9%4CM&Lff;2pC^Mxa}$Gpo#Udp!?fXGd>SMDpg~GLC$bUcty4YbZ~MM^H-0T zlxJuTOIIj8>WC*VBbLMEhTmR2MzWa&mV@Su%L0Hi zS}_-jIIHAP#b&4#-}~2tnW|m&bdXd>Xz;8AJ*qtstl?3#zPnH>jyDxG!&TMD_BVbw z%_@_~Q_5*=Y99HjSSzkSwsV!om$+^JWf>wrz|440+A6L#BXfAHWsV|R3`KxGb1N8J+clHuLIk$Ll6&xg z?ArZD;*R#9ky8?_F^s)`dUQZKQ8LrXOej6nMsAig1KEEQ;sy3!xzC`K=re%w6qIX0% z4*X6vgJ)_>?{PUxuc^$3_+N?%g$_{gcNiD@iq$N&QxV~x1JLD7o{&N*x%vwKVu*9> zfcdb|<%bIG@y*jk%>?}(#UD;C#Xh*3bu~x$fL{LK&Ppc&P=vK{wLVswxpPxY1Z@eB zg?U{=LP8%#xQtO@b9N&2A0QU;6dl8U@+x?e(-}erW`jI(3V$P(szC%lPegurk!s=H zi$i`D(U(kkvA>W7p23ksCB$>1swVIX3T-M%XwgagABKW?!$yD#V;4R8YahjH=~e{c zJ|6c%o}s>`6qn}^kZhEt5ydEHPk1RSDVluvSNCd+6R#>gV&gIrW;EJ#b)@sN!N7_v z8;i*U7H}hilq>8cUFedj!jSLLeW>@Rtl~w+)>oXVJF^0%o*1ocnZrf^ByLLv^ih9} zmm+a2t2I427M?wjBvY~2{W7tIe6wQ!GV>fm3R9##wyiOsdj%o!~m4*#;OxS_mx|?i>&D!R@7=bkOzU8cO{9l4cmuKae&JapKmmlKBYh7 zJdKGzi}dtjX%3&-^%Y7Npqs*yJ2q;#GL~qkM`NFk)lgt@6fhq5GO%al<^gA*WVI<` zIe0>YQ$89X2USKd;}E{PC(c*mxFN`qcs^M#di}~2=tR1)As#G7o47U;j&>ZI@^qgm zpf-LgW1|F54I1u2iUiD@cI2ebiNcMv4(g?-I0vYy`o1+Mn;{Hrod@nsr)1o2SX@|_ zBJ(HTUL5U0XNrKHja(NiUQ{xYR7pM;TPm?a3ZOO5zy-B6XpnGPV^N!I9}Iq=y%l#8 zNK(8Gc*g*Wg`Tg`rY5BG&D{4EMjKbMN{mHpJBUW|BCp>b+&6)2Fgbd2I=1hGYe0@! zRBi>0YF1BB=)gYpl3l+C`D^FRee}K6EXp~xcT!tY@rIP!g3eHGr&qh>wr{x$mShOj zWl%Mg_Qgm%;3KCZLq!VNV8|1^(CgJ0ogrgu%Vh(mmY$|~*t*ua8Gob>_gC#~naLhI ziM~7rA-*_SgVoTSnctxnU7vHe0{*zYxK$YsKIme`1JLBd%6H60oyKHI1O(g3NH5g) zEK_F9y`3xbd92K^2VY0o;8rYihPOKAfg>rmm<>Qa+OSY^vVwKpC#@Fxc2avVkLG(i zqR#MoHlD(HTb-w9kxUgyzinZ&Gce!nCFQs)jZqrHzq@ zLy|}9e9epmEUpz~KOMfx?PD8%BzF%GCkPsSaY&sb3pl^TM^is-#?vjw>}gK3rA8D$fdQ($%sC-qL7c%-aV`WCQ#HmP9>J1|95r;;#_ zD|`{?3tn>2!-A~zksu6AA?)Cx*qMJN41U!4+Y@auFnCpp#hgE0ZkRzQtqzesvP8ay zzt8CJmI&r=)vym4RIWqn#7z`V-T_U}Tf><2wAAlVUi4ryZD(9QPs?c}o_f(b*?4YX zX7iFB?Xh|*M8_9jS=PGW74S52zO#aN%v;`K8SUUCK04oF&KSP2FD=xy+p0S7V@J zOK~=u3l*{-`-TiPdtL!({&24K9uO1#VYi#a#e+E6%HJtjWVqfRhWub}bf&YWT!1z1 z(#W5S=jLQ+7;OWSUMJ>z%zKN=mm^4m(ZDnAA)T4W#fA=PC_+2}jpd-1D9~2#apoeF zDd)ZIXV+b)ei#q(wVu>MoVL{^HAR1{T7|qBh_^3(!8s{w>YLgIyqhZ0EHr7>Rmw&^ z8ai!IBw2ZQamF}`l8Z69`NovMH1PDiHV6FEkaSn)pkEIiFayo!M+bVi*)x=E2QMpa zSWY%}G%J_0+PO$LAC>cJMr^O!R>gz$O;aOEy2rM&A)W5{$ocS(-A-qm=fOz?b`Ui2 zmoFEc9$y`7b%A0-7VCGHgoT?MGn|eEIM*$7baNYcbtbRtR2t$fA@e>ra0KTtdro++ zite?a?r`AHkY}blkh{X{@vH*$cAh-BRwH?PAP~X`$3wRqX~gjbI0)h5nYnx&2C8dx z3I;BuJI-T+;jAzoKZqS>g$U|Q_>L)&>r$iLEg@k3--1M1U|u^TcX_z0V>wYPpP*8tyK_SE69l&|}MJ|8!|D3`1S7ze*e-7#c zizED}{?%=AT(II3KO+iWStAtJQ<$;QAa?f3o2Ni9S<;^Ztp zVBp^Pic40cnv5?_&)|{32u7t;8w@f4@h85V_3RNx=VZ6754m(w{)qfZcZOV z*FBoy)L8esZOQMdS&*#k$E(&@P}1|n@UAT$?7w|4g>P`XtF13b=h^`ouhTdrAGcnrmYnE2|=} zZ{NOcPZzEv%b~jz>Y6~c#-HU917}C2;TYs_v$xM-%?$(og3Gzw?6OI9B~_JevBNfu z0ys`@Wwp}B+k63Db?C|MeO=FG6cwZPu07p(c~vT{kbxisoN>_cG%}vc;e2o;=V-OF zyhgj)0G2Sk@Gn+#t}wrzq||%u-A01LVti@p->kW3b-K8Kwh}*D`LQdPQ^|e~270?@ z;TVRh+uz6Md@&JC&xSd%9uF)4$p!QCj%c{J9OPRW5v!|erDlxL-G%Xh=|Na_IDGXv z0Qk;CoOOjr1O-V*NQiNAEJW(vu&)k~FGA+YkAfD}Sm@RijbLwuYAb2kH z6USd)1y&zj^Mfy*QIC7u)JHlamc4cyZ5=F^rL|&A=Ci#nEAbd8IP5!1Fi|PahkF@H z17_>Pg{>!8E>`8rDn0z;L6K+km*GITPFC8Mcg^+k1XnM3=H7SowA)Thql#mXxIR94 zj2whqds(5v^9%(=Q_BQv#JPH}k{+v8X|P`XhaF$P4?nz@Wi#8>gsAh zYW(%7pMix%_C$$EA=p=O>=jBB=&6Hiq*SAHTX|F4=1dRO*xsTztt5G zFz`IyVykpTT#c6m%5iR=wvpiOt!-y=RBDYzw`mgjm}7e!x8uLYIQ4WOPDh{2C{&(Y z#>F~oPf*ozTaglQ7|4jcI&dcHa@$ZzYkOmZLLS3iQEXmc{|oL@vYu@ah_xO zLbqbnc;r4k`E4lRnpI)vlHn=%IN8{;g5C3YEEx;D+PlZuLnm=Yp<^9ZM^N;x_fS9v z2)rJkdP2*?!w(+4%B5w=#D~Se!55&K^8oJWh3t=2@KwO!=>K))8DZW8&)qYCkNejc za{D6St33K=yU@Tog1^4~>~``xXQ@tb-qb_tXw%)`eup3cj6pxY@o&HXZvpMQgi!5U z&8>#Aaw+5bI52i|75{8Us0{!% z`F)pIKnKg|xB@LgGS;O^h{xpMQy6mHK1?6Kdt|7~RF)%R`Db>p*IhBqeZdj{Tm;2u z-KUzVaUArFcVRN^9@_2H_m>VtJ|5*D#|5N>^N4Tn2Yq+H4;fj?H;0u4gX58IKQD6^ zf$3DEQY_0KLP242=E&drZ!xJvP*5Co!D>by0`DUI9*g?uykf>myphB=bumyW`R;HR z&i&;i@)@*T=4x9@BWNM$o8{omrG0~qz$g4*vbG+aoGIkS4mxX~)b2tE_{ZPZftO%y zbhGX*e!BGi=BkkeQ!lt|vmb?`MFy>dsWZc4ADCox}jN1zaI z>~ZM}8*hA;)}Nu}v|sYvTGOG6T?}LLN?Wxq6&8x?jaG;mcuS;UwIqztRs7(cK3^Yb zhD`$#5yPvSxq;Kgi6RD^j(&z~S@hY+dr(OGcjEyBkL~~?^$~mMF(YNhb=P|~Nz4nbpsn_McXhUBu&e_8E%FguVHQT0`XjJqUIoeJ# zEH1j&iMGYhXwIyJnu%HJl`?d|0!+?L+Zd>@JcmDWGmgB%co)Qfkz3-g!KrF3kx4_Ovoru=iU20jUFNpNt?dZaN>L6S1@xe zKV62E(7P_bhtc=0I$Mm2RIs{PRW>J8<@ubJtk1lhE~>8Vxpm<-`{MD*4Gg&cpZed< zMptp$B^cxdz2%+Bjg0)2jH4a_M!x9o0>jt?Pm|$k{Sih;!O8cM&4Xvi3Z==LAxFtq zj`DCmhoMG7v?t|YSH+b2QilyMI)ks zG-=XNiAa+gkd7d|Nez8;i-I5`(m_D!J<^2$Q4#5#&_kq43(^u=;4FgQ{`TkD=RfD4 z3$F{tWM<9ETJL)9=Y40Q#o~3(VhQb~4`2M&@TOV30r7C`b{Q_d&b0BKHafJ*i9ZBt zcKF69zQT!90S?9m>X5hqbXOQ4n(HI;Oi?v z8P#D|8)4CT*0Vke=bg=7v%w(}Q?5}#s#*BrEQSBUgsIUDV-)ZDyRrHM2o5}V#LbRS zZ#!e&Y@B+#T{W+n#arugPh(j>r2Lfm*{*^0wS)7ROLJ1+c^+AY*}o{M>7tYiigMVF zzVW-rp+&$qG_0s#xx{nhGh2271CAQb4!d?vGO#qcVV7KHTgIUm*ov_4R^FuIFE5gl zr4w1PB8uYCxgO`VTdm?_LRSYcprF~B-%;xcCP^p3NWBYCDbYkK*$g}LcaVX;?Y zJ=Vs3>?FEIrA)~4%hG|EjS2!Zd98EaU1EE%<$+OiNMqC8rlz~LG^y@$Q%@7U_+A90 zP3dp#g{iXA>*+&ry3B3)8Wsd*VV#&krX8o-{jz%zogb z&*+Hgx)JXYyOx?+@}$)4kQW??x^{$5h4!rP*6rgCsk(?@ZMot~o)MKBAov_+I5exP zueh@5%&m~ym;y!^FExxEWRzg&B^%|b9~c?jw4#AunqFPJ*899{6cW?)d@;wt~a zVh>ElnJOy5pukd67BB>4#5iDDMiZA~+;TDOVg=vuOJ$y@IL`3TTI?s3zj zpg6J3j~aGAy}spdIMRiWrN6O!%irxw&!&5!u#x`;WY^KmA9MCaj}h(g+H!?6H!HTy zb70`e{8lzDCOv=VD$sVXs2ckD7~>S;a(Td*_;S*S>73V=%Su&ym9LEs{sh?wYae;j z=bE$~FWR8KasA)%yEXf!Xn{14Ef)=zg2V%vOeUZkG~KBeItQnRJWTYP_6Zyd zjQ2Df#j8yCkezj2Xc!geEfuq^BygmaZv|D! zb5VpijnTt_Nj<%CvMkv2acCss@_<`}7iIdgPcFee@4X*r?3r4EGwAiUQ{@TvCAtyX zk>au`vjLcITY7Hgp1~5cnAV@4hucFNxEd=7I%Fscabni2%+17}*&HlC*3YO{di|Lj9|Qd|mW^%H)q38u!Gbuol$;~`mD=#Bx6=z3P&`R$mvFr(Vw z>OHW>+aD1-S?%KwHdyn%YFR6YDBj}gd@Ik5C!QldUIO(y%b8^$U?xB|rQymk2H)kQ zh)gW5)v3dm*vB`mq~B|Df)jMUS?x*?nw0}w-Igux*H8A!d>Cq1Yl%GfusR$SZqIN~!B3F%CG?T1LlVutHoft8U*nRV55 zyX%?h5t?DdtL;4wD^-`kyE6p#vhUn~3{hpEw#xgsyD!RvW@VHtqN!InoV2XnI9g~XOew&Gd zUb8Tmpc8c(@};!BkGkOsm9if=QQ7uhsGkmrS~cSRV|y|6;~}|P!Od_0fWp+#WPH~* z)KD`i=x)_*UQYXpD_O*!T)c-97o3LMG;OL?j=&z9S2NA-e96E&tASXZA%`LZ*^cxH zEn$13$e*i!ei8SVle_+fM)m~`?(CUj5>_&7`>P~>Bp*UHkL8S%r>>#($)K!; zKN)JCdg>{$o3*4w9wYD)1sZ%eOg(^(lFCj-O9ycV5=XPE3?fY^o~AJY-lV5X4SR;+ zCdF5IRf>Ho^K<4cH^#{mnxthh4^aMcbBC*7&~!}-vU3DUb=tf;v?H})kzoO?ebbA z{zb{G%gQ;AkM>~|PBN=pjX{X9rTIp~w__VPCBE`riq=ybs8_% zizlutKN_)}_L%frnK93{oxzZ&RRz}d zw=f`iF={*P5bS~=%?`o5yV$!_w9Vss&G(vHAJ!y<-m>g~uB&9c**bH`Wa5D6Z1$QX zm=oZKJIUM5U2{@0I`8iBZIkMJpU>Fd?+{b$xde@O(j=$hB|2Rmh{tU$DpmBEXD+w^ z8B29rM>}g*nc92tKB(T0SCwSnbQDZWaeztqxS{DTeW+n_qYrAJ&`Ue_`HG{#R|Y@} z87CQ677X(@)=pLpDXcscGGxY)$^-u5o!@Z>%>Vz{z`6JkEKcVQLlRw_chMr2fr=KV z7Q3>ai72(4^_6!o6#(P|!mg#%u6Sz9^C8+{^qFao0MR4!iMsgtF_r2m=^q4$@oBgL z2$2h8v#n0)E~T>fBOqfH5FrKWs3YK_1xfGK7M@#=pz!0h@lldKID#{kiB37$Z`gbl ziEDU+maf?TAl~Z?y1sc2l@jYjD|0<4-37GihJ9~3nhUJty{%PjPFVnQiRGcT_jPhc z(#(}2rl#&MxeA1_4;PvNvI*MloD+gwthJ>rb-VTes6FNKWLYC@u|z;2XrwYD_2`yU z-hmuzFF#cJ7d1qJI5}~zM6xi$vYFlD)D;jAV|koETmkdw@pFNxiRj^{)dYQ=0=u4k zlhj|q^9C1K$hxRQ#qV{xA-DH5?u&6NIKH6BAh?@zC;t97c9l7Nwx19i6KbhG9^{c* zi$8_z8_JLOsGeEy3FE#girF55s$4-(HNP`*c~Wnmlq7&Yi`^dFjjF$bYWz>3RO`vU zll|&m`>#7Ky1HTUtE%>3DY!ydc#Z;1fZIYlChMtNi$Tjtv<9+^%${ls`6NlG+>>C5 z?pP}-VHbJ_y~-hixcx4Z>X972s3*|45nK{`0xExNo!~3iR3`r*bXeh=b}B#uZIm00{Jo=k zf|uMHW;Y{jG8Om2-pP+p0a_Kck?gUHFUS(5c)jmdXP54RYt#alnMdLIM>erM{TIbR z|Hc@a7(PzV#(Oupgp_uf zV&an9!Z=5_7PlsiPVtcF)P!ZBj#jSo)`#3Ca^0?K(MN-z0bAK!v3>dDQo1rxtPda* zCWL`07yuyvh|z*Y)46$HtpZ=gzEnz1=eIkoE$y`uZ@>_Gw?@5Om^+Hi=zMRZb+Kke z^6g4fPy$xLp>Rv!e4HTOxrKv&R5uhj&q&Ah@!eHe?He60|z#)n-CcdiuVMQ zfEvN}AE$ONUrO!a4I9?eAy^J}s!4P6_9qbEV7LE5v0aP%*Y0FNNO`h=7QpYf2=FS{ z%ML;Q{8A84E>cZ**d68%Hjdo5rPkuB3Lnxz(D?JoZsQpkYy>74x4gWAM8oW;@%Qjv z1>kDHPQetlqX5Lv&gy_(`2OzC9a#jn-|crnvD+JZK=eb(S>F8U6@cv1e7F8$nEm5m z!uy@0%0NM}zxyz`uadh~xaamdk>o{cADXinLh^o-ol7mn*An}zXY_Ws1?i+79kQ>I zw7d=Q=g&W>4GzBdHwl;5c=#`Rfpk<`hR|_ls;rGF1C5f2-_G5j^@71zE9Jq13HrK# zzg8F}w*ImJTa@Fzr5T4Ck)$m4cWwije)#mS=KU=mnI&phh~j}?bIJdX_w~}1=QRr_ z2hYq|>EFP+%?D&$4ym>G;Tn#&(5>myq=}KnMaDp((O|@>vD%DBNAj87@?6&Dt)0_P zSZx$PlJHGqAl5M6eqhu5OB8Pn{)XU}+CBu^zxcZL+j17b_OBnK51F5)7k5=r3=ya< za4u58I!B7FAZHYgG3kcz3}wHr_|+4;T%!DKoKo!&lOW>cMC7&#h43NIuYm}fXs8+P z8C030uY%#bC2J1}@^Wy`^hoB7hQ$Bfz!CFWeL%w>6D}I2%j1px*m`wiE@V6yb8$WA zl{u@0Ha$L3UdmUcv4%^rWR4SU+lQ*dUsRj(al7w_Jj>5E5-uBd<*;~6Pr?@K37E`> z)%D{Lz%7^{XP5;evDGQ4?zvF|u#|-7uQG;;Xhph9omo@O3o%7kTrk;e+Kl%KNxc`} zA6K5hLTaeakKhKnPy0pct5UP{?4G*nDvKN5kKA2S78@4nh&cmW{xf|&fm=%QKYeYI9Zzc)v@ny^n$cud3$Pyn zuBX3z;X66i+bvw2qc06UWQ!Qz+R-%)``|TeUwutKI7kMt@I2DgYaW|qQ8M%)ndsl* zawYrrh$lwyY_0Zw8)iq+&>)fC%;9~ zI;T{jE;{&h2CGsn50{Ytt?8liiw`KDj7lZO{9JcTzLl= z&LIv9{hc<+NdeX!%$L{Kbx#8ccIpvTb;yJXy4~+8ep;QVqba*a3%l@xss$gjTMb>W ztuT8<)jKr1)0M%(P$Oud6DYgeBL-u?^v~}SR3=%V2rDFTsXe8EaY;7>b1`f9O50xw z$n}Qs53rUBngYeKyw2OZMD>ehMm2u4DZVY&3)|~&_@ixlMZb`>D3$c{G#iI6St9gv z{q}r&FYsj&2@39Hqg>CoXSD{Z_du}2N2z2hlN|Op<9LRwpI$7Kd4+;=ZB5J@66vQ* zd0c-Qrji)T1*g$6`;zg4!!!vE@s)Oi2X?f5-~F(Y3xx_R2$qD9dRY;U`vZZ>%9U=Lj2l4Gsc5u+-45nENi z$?Sos%PB@@^GUc>WyO*C3Sk9Vmdb1d(0e+}&BxMe%+1-rj~mhWbSMyD2`a$4Sz($f zp8Rl=Cb7xRKy*?&eSr9pJ*&Jv=&qA`@3+fGZmZ{PZJvw@A#k6&j~Kb14q3W73W)FE4|KL9~Vc zv|(o|qvTf{vJVXWLp!a78@y=UHN>Xr(vB?xet_*QJ4TXQl9-Fw=Q+1F9NQ78W=-aK z0Dsfl-&L4B-i)j|8cB~JFMYO$bZN0buoE86bl_5bS%4My$Z;pz0gHX?EEZRt+puX? z10T8Oyg~HV!T#KkyAwau0t{r?6ti%X)=tF3j$;S>3yN>|#p;E9yze^y`P{dK+#G$Q z@^UsiY9TwuA>WjYkG`n) zd%;AZ=GNNw%Rb{uHDg!^#yRP)yh(C*8`VN0fENtUX#kK`@uAC+=<46f6zRHoftdW{ zOZ5-SSrGz!r=YZMi2{T!=?=;(8DQg6`5XU!xp z7A;~X-`j!|Ql7uPUH<$B8}gHLB*iQoi&8#!7X}?7W{mP8_S1!i+edxQGt^5oJe+1%<-p^!@JZ!jw~Xhhn4 zljIAqERtQ*ezDS5t-?ZaK5EMf7`saMj%Mej2V7f!cZ1RpP%542QOG7uHtt{7`qDD~ zg0;mOS5`SS;lDH}mk#mi9V_MZ39H^byB|oow4Lnd^ke8hvY%^2fK+Y9|LFZ{qJit)ZEPS&ZnldSrCqZtX@c|DMeyzI9VLFI0bRt16rck+4UA9V3@Ku>TxH|b4e%oqMeTb3q+y2&3YKR<8N@Am==so?3qYF2*FCZ3D` z%^Q>Q%uDDjhai-qacxdW^?4OQLC2(W^l1Kkr)*vKD1So zQqZ1T53H(fsjE7#)#fG!eOgXnNF#NP8oACxP8F;6h*Hu5xV?{%yw5=w+4j;E0K5}uXV=knH*L!;C zwB3j%I@s`}$QH0kGS2^#Sc`H$2Vve?8!o!fMy4JC7D=mQTS3D`(~cx9O6dy7-2D|S zDN!g|iD1K8`q00qW`^%_NP!b2o3+swuaV#qm|38v79M?9XeI!Yai1(^T&Bn>#XlLZ zqKGG&23NKo4SK1%wNX;7mQ^=di?IZdyy>`fSa|5p|zh);zC-h7F1_La4n4WzH&=WYE@m zeTJ1A+Q1ik$gRDmpP4;mmw=Yqz$P_thh=?u&H-Qi@-;Hm4CFS+@AZ=7$IE-ZiK(0c zRWMXn<#F0Dgl|>ocHqDiCA#+a{-_pRkCtG2>YsT+fx zCavx3m-Te-aI_Aw(wXO7e6Z+zGOhop0M|ZGT@?q5p`VI6&6Od#m@?b#2jmu?@nJ2q z-gg0oVzJV?8Y9L09|{k^K3&+2=Ylz4s%dx?rcG@W6ul@sI#@ebvb=GV9IrA4Vb=q= zOJ%lay(SCM3{oyOO)32RXrKGue3<73F)!yoyA-qs z_AraPlJE~kllW*B(qU(S{(1KRU{v5;bo+P}X0ER?TdR8gBhN_SjzeYs%e|u#Cm5c; z%OtSt|K-L5LbG+3^aUOswMOCaF3+9$5A$xP{sXvohfh%F=b2)gM6UrJZGYLFKX1`f zBjMgpbEoN|A^{L`<^co+h`CWX2$GXDT9U&s+pkC&k%7Mqv@9co_V+eNRU+x{tF1TL+wgOBBb~WpVpW@CrsSVrz zxR1>~*l!2a5j*dLC5t7RD1dDE{6%hRBR)SkLE72hR-{}mYQFmYr-*i>?EsR6dam!( zV?Ms9QmMNNoXgJ%z#2Tv{=5GG-p%Jn?dn^1qcoaXE8BEs72bL-uJ}Kd1eH;3N>u0m z)GfDJ*UPD9sYBK$NwT1&g}+;>|F_TSms1G=tCH#~#IjK@wC=Ml_h8nt@`+>AgNMoP zT1SU07;>zJB2%KY`bpb+uMD*pz&PN@uf}t z4l$PkmH-neGneipI>EMELqqi+cM9>S`(U^CvhwvUxzNa4B3wWxojcHK_F|G2{03XO z_>;eFA|4K(m+T3A30#H!2Di9YDkpX8+ZE}-|9AO+!UhcnIt((B`LC2*&owy1`tvh5 z)3gj@{puZ?I)VPBW%sx@Z3>caJKs%$BW9@ibfFGGRdtrl=9H=!q6*Z;AqyO`n-;B| z(v+K6hqR4>=Hus5sdNoekwHq@}y8HXJyd37WM3xul9wDbk!9=iLLL zdPwCNhSWGAqbvf(1+(oA67Oukui;?VADK~-(drm3?UQ`2(zi$KY^`3TH6)}v3~_K( zIimY=yyiZM$q9geS2G;rLAw9bjhfRvXQM0`!iCJ5r~T1Qo$S!Rvc zHOzD>FXT7i_H~6Bz5dYBF7cs-=BPJ%$rEw>LhPXi0~)dWbBAeGGzrYBlF{Xf7vW#B zX#`zK5|p$ld(rZgY~%Q3599bt^s9!G(8`r!jM&VcrSDQld2h9K)$EjXh3lM**1&jq z%zaXqbN*s27x1&PYv$Td2Bm~6=h1iC{&0`X?;)^)mY)YL-^)5vv8B2%!2GF`w%-XY z@msQzIMfo)j&=7$4z%a^-`b;09nJ1tuq4s|=eQdBYzo^q&O;$T9H2C) za^XeV%$u&89y1Gp4oPYCiEfgn!Bs!^k&lfkQ(BN$THK)Zr|fXXWz7UEVRO`BeNLtP zPtwm*w#5dB_SQO-#sMmmx z4QO>m;&pRf7U{-7OO-qIkqKsm?IgllQDd!IZ7Z0TJ6YkhJ7b{(%C|Q%v6S~{d=@1& zd$CndNh@gan{{b`5-6!*P9ZkG!4YgEF&T9Bg2h(c0)!0r_m6%AY}J#?`%=ss8)Gx?>KQr>j3sxqx0x^hP5~v`-`j!QpB&4PWDw=qxrc zNt;viE3*Etek9tpiKA5W%2Xw{ZOy(4mQF}@Tda`#dA;aq&N^mt!X0wDWyikL;9C#b z6xY>M;^e2&l4#c~bG1gJ7lbd5>my~i#8h*Ym|n5+iK+9!^pwR;Zff6S#5dv_*rsue9AN49xP zr$47dP9zChaGJZY@|}jVy*0(d-I)w88dr&vGtA;y&lq8-8@jeejYLScjEd4(nlfpM4^`j*bsm;>G5gk@4lspq%nt8oF=9 zK~y%FQkh`s5Ghgml^aghw>amV8Y?C1vr=v9Gb9D+dI)a9kz>8XThhJrzTF9!R_~!c zaQ!xCG-UG>keW!+X66-F)t0FgCf19{6Va}+#ELs}2`Q)egnZ-0ukwd3&YAnHH`>#z z4KuDx9}Cy$znbv4ex`IIjI{_Fe0Z+yHV}@T%g4O3K(?eZjoF|F8R?iRmV>*rcIBn2 z88mh`+bATzd<_=NB!stwX}W3j9Y<%(Kd0HR6pSFVA!NHLIbMbt^rsu+RWO%wlCnBx zl#sGjt7^R5orte$Nf0GW*!Eq2c6RyXr~Jtn`P?&XSBw}#K}Ru?g$Yg(x`uf+v0DU` z`;cEzc`lshMPWT|AE&U*hLv0mXMYu>?a-;f9<1fBLgKo!kdE!41JB_s_G*d%Wgp|X zllw@<+z&dkvpL};eJo22a$gr17G)xXg|%^=2QvoTw0-a_%$aZTYBZruloUnfPbBH0 zSqY78Vnfr0602`EXx&!dbJJt3Go|9CGT>5D-890)x#CdMj3nAn({TsLspI-k9I~3p zI|rvyB|fav23PQ8tvtLzd2Oga(qo!4=l%H3pRx9WFiuS7?6-Q*GdFj7MkxgjKk&*l z2vhJq!>kAnFRPWba5{#NwQXleOhAj6gD|3$5_{G01As3>(6HwPnE_H&rI~3Kd{Qt-Kxn%g6+;+i%Sb59))P2PnJ}_*j@+=1Y$+ zDro7=S_IG$QRk$efGolBl^I9MEivh9p&XYfH8Gm!}V&%ky2GqRt$?$bw2!#m*9 z_Z8I-)zGqIDQcl&H7w+g{-yp)*o+Ykm7{vZ$Q&lh((&2^cV6zEj)NJ2a9*C|6B1px z2MiuOX1@K9s)(^hRw3muG}M7sK=KlkeV6TO=4+ZL>s(xdVLEv~()A@} zC<`JKicrNHhrJ|=DRQ&Rm9N%O3Fsm3XV*=0)?}cPnRMpz5C<}&^z`L>XL~j9OX(R| z-YzC>8fIsEGQJh-T5T9s7EGA*_(H74c9Z)A4|cgE?@{ejHWR5xsycs=hEM2pf@knj zs~Qryv{^|kwwm~2Qnp;R$O?5Llt8dD$L5AdPK9cnZM)qrNC79i@;Zy>S#v{s{J6>S zFlZm?ps%2%i|?j#_5dfRv%FTzLezm;Vz}=nlH5_Nrm#Ul~6`D=a$@$AF)kA9!yLRth-bi$5K|A#KLCxDgQA?x{QS0m4}m@ zls6!Wr8ZH z&#QqL8!f(~|Kn}ZOPlYexmr^(Mr<~vOg76)4laKi6Q!zs#~pr6jhYspb?vw!Fz&V` zZMWu5Qg{s*vJn$J&Ovp1Fcp*-H+;?X`H)!O{`X`FjbNy>{td2wSicexjz!PWj7(Pq z-jfgaLW>}M*B8o7vE07W8N8dS=i|&5-QF@*ZFotxbG&dO98VJvyUKwNmo8k{_8IHp zg-Nr{mM0cBi$|~+bamNh&Bu}yTeL6b7C0vw#yq09zU#+icSvpd)yF9_AU_X~Rt1#z z_n`)sy&$wU%NZ-N8J8M0&M2}5;$b5XzP5CLt{bW_#7H0{N9oO$?&T_Y8=0ML^_A-s zG}6XNbz9r9wk)J?31Hw!S=SS{eR6o+@8XMYLoQD+X5j^ovZlsDY`hFFe9xG=v+rGj zUdHSlpfLg;b=C26F7ZF#2uJ(ExGf$28i>Gw=v*&+65 zFPfe!)+j}Gl@vQIG1T6QiI}1M#c#>2@E-ItLIriXA@uePDMLg`*BR0^|ma>Z&y*HHnG}agTS-_(dV)rjt zSv%X`q2l2wya#jQN2%0s`4T#Pdd~Njg)~5QLzfsYFlQF5W4y9L?jj+g8jyAnw zRViKV739qbvrPiZy2WPi$|z-3W4q|~ww3?J*xEH6Bn$I{z@t3~S)r+RnudDTeK+Nl zMb5z@@S1MbT+^5br}k)EQrAtBhA_R9SrcAtlc2KE>XWLUPqWZs*U2+Gw_cnn+`eyQ zgt(hI$C;H!S^ZYvRA#P@%YJP#nJ#Gi1JfobOM%h(zq=jqQW5o+l#|Gy)=VKMn zAN!gAzW$|E4P2lCp&)#BzWX?daAI!yh^*iba+G`&E*5)dn%+_+?(XI?s4Ba0}-F$C=Xbt4vqTP7wct#+%%Q{{x?Pa5AuA! zlySl26dc?vvRCSQ+b4;}-a;x@^ijFu{^X8e4ES_NDGn7FP$7l?YC|h%wIDpcGR6L% z00L28dD!`r99TOxz^x>!_swuQpH7|-;acGQ@9+f$gz#}U z*ltnpi`n_@_ljh&>;DQ*pz1o#xD_;cXM>c_Pzx8pum9KgQ#~E<`u{IC3cUUwZ`$Gb choh}QK9&>{I+k}ko;^$+aZfhymhqqe1-M|=SpWb4 delta 70037 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

    vN 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 QHBoxLayout: left_layout.addWidget(QLabel( self.word_dict.get("ai_code_review_gui_label_code_to_send"))) self.code_input = QTextEdit() + use_fixed_pitch_font(self.code_input) self.code_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) left_layout.addWidget(self.code_input) diff --git a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py index da1d8b86..31350d56 100644 --- a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py @@ -25,6 +25,7 @@ from pybreeze.utils.header_tools.header_merge import stored_header_name from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.pybreeze_ui.error_text import error_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font # The single target that generates JSON rather than Python _JSON_TARGET = "apitestka_action" @@ -47,6 +48,7 @@ def __init__(self, main_window=None) -> None: self.input_label = QLabel(word.get("curl_import_input_label")) self.input_edit = QTextEdit() + use_fixed_pitch_font(self.input_edit) self.input_edit.setPlaceholderText(word.get("curl_import_input_placeholder")) self.input_edit.setAcceptRichText(False) @@ -63,6 +65,7 @@ def __init__(self, main_window=None) -> None: self.output_label = QLabel(word.get("curl_import_output_label")) self.output_edit = QTextEdit() + use_fixed_pitch_font(self.output_edit) self.output_edit.setReadOnly(True) # Cross-tool actions: hand the parsed parts to the tool that specialises diff --git a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py index b59194b7..f11a12a1 100644 --- a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py @@ -12,6 +12,7 @@ from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.pybreeze_ui.thread_keeper import let_run_out +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font from pybreeze.utils.diff_tools.text_diff import Comparison, DiffSummary, compare_texts # A unified diff line's first characters -> JEditor theme colour (a dark and a @@ -87,9 +88,11 @@ def __init__(self, main_window=None) -> None: self.left_label = QLabel(word.get("diff_left_label")) self.left_edit = QTextEdit() + use_fixed_pitch_font(self.left_edit) self.left_edit.setAcceptRichText(False) self.right_label = QLabel(word.get("diff_right_label")) self.right_edit = QTextEdit() + use_fixed_pitch_font(self.right_edit) self.right_edit.setAcceptRichText(False) inputs = QHBoxLayout() @@ -107,6 +110,7 @@ def __init__(self, main_window=None) -> None: self.summary_label = QLabel("") self.output_edit = QTextEdit() + use_fixed_pitch_font(self.output_edit) self.output_edit.setReadOnly(True) self._highlighter = UnifiedDiffHighlighter(self.output_edit.document()) diff --git a/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py index 12657620..8ff80651 100644 --- a/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py @@ -25,6 +25,7 @@ from pybreeze.utils.file_process.read_capped import read_text_capped from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.pybreeze_ui.error_text import error_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font # The single target that generates JSON rather than Python _JSON_TARGET = "apitestka_action" @@ -87,6 +88,7 @@ def __init__(self, main_window=None) -> None: self.output_label = QLabel(word.get("curl_import_output_label")) self.output_edit = QTextEdit() + use_fixed_pitch_font(self.output_edit) self.output_edit.setReadOnly(True) self.actions = OutputActions( diff --git a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py index 876ecb22..1b9965e0 100644 --- a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py @@ -12,6 +12,7 @@ from pybreeze.utils.json_format.json_process import minify_json, reformat_json from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.pybreeze_ui.error_text import error_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font class JsonFormatGUI(QWidget): @@ -29,6 +30,7 @@ def __init__(self, main_window=None, initial_json: str | None = None) -> None: self.input_label = QLabel(word.get("json_format_input_label")) self.input_edit = QTextEdit() + use_fixed_pitch_font(self.input_edit) self.input_edit.setPlaceholderText(word.get("json_format_input_placeholder")) self.input_edit.setAcceptRichText(False) @@ -43,6 +45,7 @@ def __init__(self, main_window=None, initial_json: str | None = None) -> None: self.output_label = QLabel(word.get("json_format_output_label")) self.output_edit = QTextEdit() + use_fixed_pitch_font(self.output_edit) self.output_edit.setReadOnly(True) self.actions = OutputActions( diff --git a/test/test_utils/test_code_boxes_fixed_pitch.py b/test/test_utils/test_code_boxes_fixed_pitch.py new file mode 100644 index 00000000..921da0c5 --- /dev/null +++ b/test/test_utils/test_code_boxes_fixed_pitch.py @@ -0,0 +1,70 @@ +"""The text boxes that hold code show it in the fixed-pitch font. + +In the interface's proportional font, indentation and the columns of a diff did +not line up. +""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.fixed_pitch import fixed_pitch_font + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +def _diff(): + from pybreeze.pybreeze_ui.tools_gui.diff_gui import DiffGUI + return DiffGUI(None) + + +def _curl(): + from pybreeze.pybreeze_ui.tools_gui.curl_import_gui import CurlImportGUI + return CurlImportGUI(None) + + +def _har(): + from pybreeze.pybreeze_ui.tools_gui.har_import_gui import HarImportGUI + return HarImportGUI(None) + + +def _json_format(): + from pybreeze.pybreeze_ui.tools_gui.json_format_gui import JsonFormatGUI + return JsonFormatGUI(None) + + +def _ai_code_review(): + from pybreeze.pybreeze_ui.connect_gui.url.ai_code_review_gui import AICodeReviewClient + return AICodeReviewClient() + + +CODE_BOXES = [ + (_diff, "left_edit"), (_diff, "right_edit"), (_diff, "output_edit"), + (_curl, "input_edit"), (_curl, "output_edit"), + (_har, "output_edit"), + (_json_format, "input_edit"), (_json_format, "output_edit"), + (_ai_code_review, "code_input"), +] + + +@pytest.mark.parametrize(("build", "box"), CODE_BOXES, + ids=[f"{build.__name__[1:]}.{box}" for build, box in CODE_BOXES]) +def test_the_box_uses_the_fixed_pitch_font(app, build, box): + widget = build() + view = getattr(widget, box) + family = fixed_pitch_font().family() + + assert view.font().family() == family + # In the view's own sheet too: the theme's sheet overrides setFont + assert family in view.styleSheet() + widget.close() From 8d60e5180bbf72456759735bf0eb7dc95e7aa1c3 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:27:01 +0800 Subject: [PATCH 046/166] Tell apart labels that read the same --- README/README_zh-TW.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- pybreeze/extend_multi_language/extend_english.py | 4 ++-- .../extend_traditional_chinese.py | 2 +- test/test_utils/test_language_parity.py | 16 ++++++++++++++++ 7 files changed, 32 insertions(+), 6 deletions(-) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 7dfad8f2..774837a2 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -143,7 +143,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: *把 Mermaid `flowchart` 貼進匯入器後自動排版的結果。* -以 `QGraphicsScene` 打造的所見即所得編輯器:矩形、圓角矩形、橢圓與菱形節點,可加上連線標籤的貝茲曲線連線,還有自由文字與圖片。Mermaid `flowchart`/`graph` 匯入會執行 Sugiyama 式排版(分層、減少交叉、跨軸對齊)。可儲存與開啟 `.diagram.json`,匯出為 PNG 或 SVG,並支援復原/重做、對齊、均分、格線、吸附與縮放。從 URL 下載的圖片會經過 SSRF 驗證並有大小上限。 +以 `QGraphicsScene` 打造的所見即所得編輯器:矩形、圓角矩形、橢圓與菱形節點,可加上連線標籤的貝茲曲線連線,還有自由文字與圖片。Mermaid `flowchart`/`graph` 匯入會執行 Sugiyama 式排版(分層、減少交叉、跨軸對齊)。可儲存與開啟 `.diagram.json`,匯出為 PNG 或 SVG,並支援復原/重做、對齊、均分、格線、貼齊與縮放。從 URL 下載的圖片會經過 SSRF 驗證並有大小上限。 ### SSH 用戶端 — 終端機與遠端檔案樹並排 diff --git a/architecture_explore.md b/architecture_explore.md index c61f12cd..2a479e64 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 133 個 `test_*.py`、2416 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 133 個 `test_*.py`、2418 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 5b089863..85a17a40 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1034,3 +1034,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Docs**: redone under dark_amber at their sizes with the same content: `tool_curl_import.png`, `tool_curl_import_action.png`, `tool_har_import.png` (a HAR of the same four requests), `tool_diff.png`, `ai_code_review.png`, and the JSON Format panel of `tools_montage_a.png`; `architecture_explore.md` (test count). - **Files**: `pybreeze/pybreeze_ui/tools_gui/diff_gui.py`, `pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py`, `pybreeze/pybreeze_ui/tools_gui/har_import_gui.py`, `pybreeze/pybreeze_ui/tools_gui/json_format_gui.py`, `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, the six images, `architecture_explore.md` - **Open items**: none. + +## U-20260924-315 · 2026-09-24 · Tell apart labels that read the same · #fix #i18n + +- **What**: rendering every tab in Traditional Chinese showed the diagram toolbar with two controls side by side both reading 對齊: the Align menu and the Snap box. Listing every Traditional Chinese string shared by two different English ones found two more slips, in English: the prompt editors' docks were titled "CoT PromptEditor" and "Skill PromptEditor", their tabs "CoT Prompt Editor" and "Skill Prompt Editor". +- **Fix**: `diagram_editor_action_snap` is 貼齊 (the Traditional Chinese README said 吸附 for it, and now says 貼齊 like the toolbar); the two dock titles read like their tabs. The other shared strings are one control each in different places (連線 for the connection tool and the connection property group, 外掛 for the menu and a column). +- **Tests**: `TestLabelsAreTold` in `test_language_parity.py`: the Snap box and the Align menu differ in both languages, and each prompt editor's dock title equals its tab label. Both failed before the change. +- **Result / numbers**: 2418 tests in 133 files. `test_language_parity.py` passes (13). +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `README/README_zh-TW.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0d4a303d..5ad66ac9 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-315 | 2026-09-24 | Tell apart labels that read the same | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-314 | 2026-09-24 | Code in the tool tabs in the fixed-pitch font | #feature #tools #readme | [2026-09-b](2026-09-b.md) | | U-20260924-313 | 2026-09-24 | Consolas before Courier New for terminal output | #fix #ui | [2026-09-b](2026-09-b.md) | | U-20260924-312 | 2026-09-24 | Move the fixed-pitch font helper out of terminal_view | #refactor #ui | [2026-09-b](2026-09-b.md) | @@ -396,4 +397,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 112 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 113 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index cb50f631..b1ce09f9 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -356,8 +356,8 @@ "extend_tools_menu_skill_prompt_editor_dock_action": "Skill Prompt Editor Dock", "extend_tools_menu_ssh_client_dock_title": "SSH Client", "extend_tools_menu_ai_code_review_dock_title": "AI Code-Review", - "extend_tools_menu_cot_prompt_editor_dock_title": "CoT PromptEditor", - "extend_tools_menu_skill_prompt_editor_dock_title": "Skill PromptEditor", + "extend_tools_menu_cot_prompt_editor_dock_title": "CoT Prompt Editor", + "extend_tools_menu_skill_prompt_editor_dock_title": "Skill Prompt Editor", "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt Dock", "extend_tools_menu_skill_prompt_send_dock_title": "Skill Send GUI", # CoT code-review GUI diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index fb5927b0..2bb6f8d6 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -652,7 +652,7 @@ "diagram_editor_action_undo": "復原", "diagram_editor_action_redo": "重做", "diagram_editor_action_grid": "格線", - "diagram_editor_action_snap": "對齊", + "diagram_editor_action_snap": "貼齊", # Diagram Editor — 對齊 "diagram_editor_align_menu": "對齊", "diagram_editor_align_left": "靠左對齊", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index e4ef08db..48253554 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -84,6 +84,22 @@ def test_traditional_chinese_uses_taiwan_terms(self): assert not {key: value for key, value in ZH.items() if re.search("終端(?!機)", value)} +class TestLabelsAreTold: + def test_side_by_side_controls_are_named_apart(self): + # The diagram toolbar's Align menu and Snap box both read 對齊 + for first, second in [("diagram_editor_action_snap", "diagram_editor_align_menu")]: + assert EN[first] != EN[second] + assert ZH[first] != ZH[second] + + def test_a_dock_is_titled_like_its_tab(self): + # The prompt editors' docks said "CoT PromptEditor" + for tab, dock in [ + ("extend_tools_menu_cot_prompt_editor_tab_label", "extend_tools_menu_cot_prompt_editor_dock_title"), + ("extend_tools_menu_skill_prompt_editor_tab_label", "extend_tools_menu_skill_prompt_editor_dock_title")]: + assert EN[dock] == EN[tab] + assert ZH[dock] == ZH[tab] + + class TestCodeKeysAreDefined: def test_every_get_key_exists_in_dict(self): # A typo'd key (e.g. the "cot_cot_..." double prefix) makes get() return From 7aae40ab6c49c315fb069a12961299b34e1f499c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:35:03 +0800 Subject: [PATCH 047/166] Reword the Traditional Chinese Open in editor tab button --- docs/updates/2026-09-b.md | 8 ++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_traditional_chinese.py | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 85a17a40..3085597e 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1043,3 +1043,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2418 tests in 133 files. `test_language_parity.py` passes (13). - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `README/README_zh-TW.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-316 · 2026-09-24 · Reword the Traditional Chinese Open in editor tab button · #fix #i18n + +- **What**: every tool tab's button that opens the output in an editor tab read 開成編輯器分頁 in Traditional Chinese, which does not read as a sentence; the buttons beside it say 在 URL 解析/組建器開啟網址, 在分析器開啟標頭. +- **Fix**: `output_actions_open_editor` is 在編輯器分頁開啟, in the same pattern. +- **Tests**: `test_language_parity.py` passes. A wording change; no behaviour to test. +- **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 5ad66ac9..107b7999 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-316 | 2026-09-24 | Reword the Traditional Chinese Open in editor tab button | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-315 | 2026-09-24 | Tell apart labels that read the same | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-314 | 2026-09-24 | Code in the tool tabs in the fixed-pitch font | #feature #tools #readme | [2026-09-b](2026-09-b.md) | | U-20260924-313 | 2026-09-24 | Consolas before Courier New for terminal output | #fix #ui | [2026-09-b](2026-09-b.md) | @@ -397,4 +398,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 113 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 114 | diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 2bb6f8d6..cd424355 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -426,9 +426,9 @@ "har_import_generate_error": "無法產生腳本:{error}", "har_import_not_utf8": "不是 UTF-8 文字檔", "har_import_read_error": "無法開啟檔案:{error}", - # 共用輸出動作(複製 / 開成分頁 / 存檔) + # 共用輸出動作(複製 / 在編輯器分頁開啟 / 存檔) "output_actions_copy": "複製", - "output_actions_open_editor": "開成編輯器分頁", + "output_actions_open_editor": "在編輯器分頁開啟", "output_actions_save": "存成檔案...", "output_actions_editor_tab_label": "產生內容", "output_actions_save_failed_title": "沒有存檔", From 5a5ec563a2c58fc01f96546848be4a3728d86c79 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:47:58 +0800 Subject: [PATCH 048/166] Say what the CoT review's step selector is --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_english.py | 4 +++- .../extend_traditional_chinese.py | 4 +++- .../code_review/cot_code_review_gui.py | 14 ++++++++++++-- .../test_utils/test_code_boxes_fixed_pitch.py | 19 +++++++++++++++++++ 7 files changed, 49 insertions(+), 6 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 2a479e64..2735795d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 133 個 `test_*.py`、2418 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 133 個 `test_*.py`、2420 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 3085597e..f8b33a21 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1051,3 +1051,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Tests**: `test_language_parity.py` passes. A wording change; no behaviour to test. - **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py` - **Open items**: none. + +## U-20260924-317 · 2026-09-24 · Say what the CoT review's step selector is · #fix #ai + +- **What**: the CoT Code Review tab's step selector (which answer is shown) stood alone and empty halfway down the left of the panel before a run, with nothing to say what it was. The box the code goes into showed it in the proportional font, and its Traditional Chinese placeholder said the prompt would be shown there (這裡會顯示要傳送的 Prompt 內容) while the English one asked for the code. +- **Change**: the selector has a "Step:" (步驟:) label above it, sits at the top of its column, and reads "No answers yet" (還沒有回覆) until the first answer. The code box uses `use_fixed_pitch_font()` like the other code boxes (U-20260924-314); its placeholder is "Paste the code to review here" (在這裡貼上要審查的程式碼). +- **Tests**: `test_code_boxes_fixed_pitch.py` covers the CoT code box and `test_the_cot_step_selector_says_what_it_is_before_any_answer` (placeholder and label). The CoT lifecycle and session tests and `test_language_parity.py` pass (42 with them). +- **Result / numbers**: 2420 tests in 133 files. `ruff check` clean. Rendered under dark_amber to check the layout. +- **Files**: `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 107b7999..3661c1d3 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-317 | 2026-09-24 | Say what the CoT review's step selector is | #fix #ai | [2026-09-b](2026-09-b.md) | | U-20260924-316 | 2026-09-24 | Reword the Traditional Chinese Open in editor tab button | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-315 | 2026-09-24 | Tell apart labels that read the same | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-314 | 2026-09-24 | Code in the tool tabs in the fixed-pitch font | #feature #tools #readme | [2026-09-b](2026-09-b.md) | @@ -398,4 +399,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 114 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 115 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index b1ce09f9..78413c58 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -364,7 +364,9 @@ "cot_gui_window_title": "Prompt Sender UI", "cot_gui_label_api_url": "API URL:", "cot_gui_placeholder_api_url": "Please enter the API URL to send, e.g. http://127.0.0.1:5000/api", - "cot_gui_placeholder_code_paste_area": "You can put the code to be sent", + "cot_gui_placeholder_code_paste_area": "Paste the code to review here", + "cot_gui_label_step": "Step:", + "cot_gui_placeholder_no_answers": "No answers yet", "cot_gui_label_prompt_area": "Prompt Area", "cot_gui_label_response_area": "Response Area", "cot_gui_button_send": "Start Sending", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index cd424355..4d347bb2 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -354,7 +354,9 @@ "cot_gui_window_title": "Prompt 傳送介面", "cot_gui_label_api_url": "API URL:", "cot_gui_placeholder_api_url": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", - "cot_gui_placeholder_code_paste_area": "這裡會顯示要傳送的 Prompt 內容", + "cot_gui_placeholder_code_paste_area": "在這裡貼上要審查的程式碼", + "cot_gui_label_step": "步驟:", + "cot_gui_placeholder_no_answers": "還沒有回覆", "cot_gui_label_prompt_area": "傳送資料區域", "cot_gui_label_response_area": "回傳區域", "cot_gui_button_send": "開始傳送", diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py index 5f7cbfc5..3b33545a 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py @@ -8,6 +8,7 @@ from pybreeze.pybreeze_ui.extend_ai_gui.code_review.code_review_thread import SenderThread from pybreeze.pybreeze_ui.thread_keeper import let_run_out from pybreeze.pybreeze_ui.exact_text import exact_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font class CoTCodeReviewGUI(QWidget): @@ -31,18 +32,27 @@ def __init__(self): # 傳送資料區域 self.code_paste_area = QTextEdit() + use_fixed_pitch_font(self.code_paste_area) self.code_paste_area.setPlaceholderText( language_wrapper.language_word_dict.get("cot_gui_placeholder_code_paste_area")) layout.addWidget(QLabel(language_wrapper.language_word_dict.get("cot_gui_label_prompt_area"))) layout.addWidget(self.code_paste_area) # 回傳區域 - self.response_selector = QComboBox() # 改用 ComboBox + # The step whose answer is shown, labelled and at the top: alone and + # empty it sat halfway down the panel with nothing to say what it was + self.response_selector = QComboBox() + self.response_selector.setPlaceholderText( + language_wrapper.language_word_dict.get("cot_gui_placeholder_no_answers")) self.response_view = QTextEdit() self.response_view.setReadOnly(True) # 可複製但不可編輯 + step_layout = QVBoxLayout() + step_layout.addWidget(QLabel(language_wrapper.language_word_dict.get("cot_gui_label_step"))) + step_layout.addWidget(self.response_selector) + step_layout.addStretch() hbox_layout = QHBoxLayout() - hbox_layout.addWidget(self.response_selector, 2) + hbox_layout.addLayout(step_layout, 2) hbox_layout.addWidget(self.response_view, 5) layout.addWidget(QLabel(language_wrapper.language_word_dict.get("cot_gui_label_response_area"))) diff --git a/test/test_utils/test_code_boxes_fixed_pitch.py b/test/test_utils/test_code_boxes_fixed_pitch.py index 921da0c5..7fc86406 100644 --- a/test/test_utils/test_code_boxes_fixed_pitch.py +++ b/test/test_utils/test_code_boxes_fixed_pitch.py @@ -48,12 +48,18 @@ def _ai_code_review(): return AICodeReviewClient() +def _cot_code_review(): + from pybreeze.pybreeze_ui.extend_ai_gui.code_review.cot_code_review_gui import CoTCodeReviewGUI + return CoTCodeReviewGUI() + + CODE_BOXES = [ (_diff, "left_edit"), (_diff, "right_edit"), (_diff, "output_edit"), (_curl, "input_edit"), (_curl, "output_edit"), (_har, "output_edit"), (_json_format, "input_edit"), (_json_format, "output_edit"), (_ai_code_review, "code_input"), + (_cot_code_review, "code_paste_area"), ] @@ -68,3 +74,16 @@ def test_the_box_uses_the_fixed_pitch_font(app, build, box): # In the view's own sheet too: the theme's sheet overrides setFont assert family in view.styleSheet() widget.close() + + +def test_the_cot_step_selector_says_what_it_is_before_any_answer(app): + from je_editor import language_wrapper + from PySide6.QtWidgets import QLabel + + widget = _cot_code_review() + words = language_wrapper.language_word_dict + + # Empty and unlabelled, it sat halfway down the panel saying nothing + assert widget.response_selector.placeholderText() == words.get("cot_gui_placeholder_no_answers") + assert words.get("cot_gui_label_step") in [label.text() for label in widget.findChildren(QLabel)] + widget.close() From c0850aa5fd7430c11db2eaeb3402ccbc4d74da7c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:54:08 +0800 Subject: [PATCH 049/166] Say that the SSH terminal shows output line by line --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/updates/2026-09-b.md | 8 ++++++++ docs/updates/README.md | 3 ++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a7c7f118..5e2838f5 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 1e38f988..a8666917 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 774837a2..94fd4cdc 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index f8b33a21..745d0d82 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1060,3 +1060,11 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2420 tests in 133 files. `ruff check` clean. Rendered under dark_amber to check the layout. - **Files**: `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-318 · 2026-09-24 · Say that the SSH terminal is line by line · #docs #ssh #readme + +- **What**: the READMEs listed what the SSH terminal does (colours, the pty size, `clear` and `reset`, Interrupt) but not what it does not: it shows output line by line and drops cursor movement (`strip_terminal_controls` in `utils/terminal_text.py`), so a program that draws on the whole screen by moving the cursor, such as `vim` or `htop`, comes out garbled. +- **Change**: the SSH Client paragraph of `README.md`, `README/README_zh-TW.md` and `README/README_zh-CN.md` says so. +- **Tests**: none; a documentation change. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 3661c1d3..07e43d5d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-318 | 2026-09-24 | Say that the SSH terminal is line by line | #docs #ssh #readme | [2026-09-b](2026-09-b.md) | | U-20260924-317 | 2026-09-24 | Say what the CoT review's step selector is | #fix #ai | [2026-09-b](2026-09-b.md) | | U-20260924-316 | 2026-09-24 | Reword the Traditional Chinese Open in editor tab button | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-315 | 2026-09-24 | Tell apart labels that read the same | #fix #i18n | [2026-09-b](2026-09-b.md) | @@ -399,4 +400,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 115 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 116 | From c9a1ecd566a210438b9ce37340aee436df7d3f5e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Thu, 24 Sep 2026 23:59:02 +0800 Subject: [PATCH 050/166] Drop the unused ReEdgeGPT language entries --- architecture_explore.md | 2 +- docs/updates/2026-09-b.md | 9 +++++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 6 --- .../extend_traditional_chinese.py | 6 --- test/test_utils/test_language_keys_used.py | 40 +++++++++++++++++++ 6 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 test/test_utils/test_language_keys_used.py diff --git a/architecture_explore.md b/architecture_explore.md index 2735795d..77c7d3ab 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 133 個 `test_*.py`、2420 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 134 個 `test_*.py`、2421 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index 745d0d82..d40cb59d 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1068,3 +1068,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Tests**: none; a documentation change. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` - **Open items**: none. + +## U-20260924-319 · 2026-09-24 · Drop the ReEdgeGPT words no menu asks for · #cleanup #i18n + +- **What**: both language dictionaries still defined the five `tools_menu_re_edge_gpt_*` words (ReEdgeGPT, Open ReEdgeGPT Doc, ...) of the Bing GPT menu that `dd6fcf9` removed in March 2025; nothing in PyBreeze or JEditor reads them. +- **Change**: the ten entries and the `# Tools Menu` comment left heading nothing are gone from `extend_english.py` and `extend_traditional_chinese.py`. +- **Tests**: new `test_language_keys_used.py`: every key of PyBreeze's English dictionary appears as a string literal in the package outside the dictionaries, or starts with a prefix a key is built from at run time (`error_text_`, `run_window_`, the SSH file viewer's, the header analyzer's) or that JEditor's plugin browser reads (`plugin_browser_`). It failed on the five keys before the change. `test_language_parity.py` passes (13). +- **Result / numbers**: 2421 tests in 134 files. The full suite at `5a5ec56` passed (2405 passed, 15 skipped). `ruff check` clean. +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_keys_used.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 07e43d5d..6be7bf6e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-319 | 2026-09-24 | Drop the ReEdgeGPT words no menu asks for | #cleanup #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-318 | 2026-09-24 | Say that the SSH terminal is line by line | #docs #ssh #readme | [2026-09-b](2026-09-b.md) | | U-20260924-317 | 2026-09-24 | Say what the CoT review's step selector is | #fix #ai | [2026-09-b](2026-09-b.md) | | U-20260924-316 | 2026-09-24 | Reword the Traditional Chinese Open in editor tab button | #fix #i18n | [2026-09-b](2026-09-b.md) | @@ -400,4 +401,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 116 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 117 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 78413c58..22dc2e32 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -108,12 +108,6 @@ "install_menu_prthinker": "Install prthinker (code review)", "install_menu_tools_install_menu_label": "Tools", "install_menu_tools_install_build_tools": "Install Build Tools", - # Tools Menu - "tools_menu_re_edge_gpt_label": "ReEdgeGPT", - "tools_menu_re_edge_gpt_doc_label": "Open ReEdgeGPT Doc", - "tools_menu_re_edge_gpt_doc_tab_label": "ReEdgeGPT Doc", - "tools_menu_re_edge_gpt_github_label": "Open ReEdgeGPT GitHub", - "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", # Test Pioneer Menu "test_pioneer_label": "TestPioneer", "test_pioneer_github_label": "Open TestPioneer GitHub", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 4d347bb2..77f1bd25 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -103,12 +103,6 @@ "install_menu_prthinker": "安裝 prthinker(程式碼審查)", "install_menu_tools_install_menu_label": "工具", "install_menu_tools_install_build_tools": "安裝 Build Tools", - # Tools Menu - "tools_menu_re_edge_gpt_label": "ReEdgeGPT", - "tools_menu_re_edge_gpt_doc_label": "開啟 ReEdgeGPT 文件", - "tools_menu_re_edge_gpt_doc_tab_label": "ReEdgeGPT 文件", - "tools_menu_re_edge_gpt_github_label": "開啟 ReEdgeGPT GitHub", - "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", # Test Pioneer Menu "test_pioneer_label": "TestPioneer", "test_pioneer_github_label": "開啟 TestPioneer GitHub", diff --git a/test/test_utils/test_language_keys_used.py b/test/test_utils/test_language_keys_used.py new file mode 100644 index 00000000..d074b88e --- /dev/null +++ b/test/test_utils/test_language_keys_used.py @@ -0,0 +1,40 @@ +"""Every word the language dictionaries define is still asked for somewhere.""" +from __future__ import annotations + +import re +from pathlib import Path + +from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict + +PACKAGE = Path(__file__).resolve().parents[2] / "pybreeze" + +# Keys whose name is put together at run time, and JEditor's own widgets' +# (its plugin browser reads plugin_browser_*, which PyBreeze translates) +BUILT_OR_READ_ELSEWHERE = ( + "error_text_", # error_text: ERROR_TEXT_KEY_PREFIX + an exception tag + "run_window_", # run_notice.RUN_NOTICE_KEY_PREFIX + "ssh_file_viewer_context_menu_action_", # ssh_file_viewer_widget: f"...{name}" + "ssh_file_viewer_tree_header_", + "ssh_file_viewer_type_", + "header_analyzer_level_", # header_analyzer_gui: f"...{finding.level}" + "header_finding_", + "plugin_browser_", +) + + +def _source_outside_the_dictionaries() -> str: + return "\n".join( + path.read_text(encoding="utf-8") + for path in PACKAGE.rglob("*.py") + if "extend_multi_language" not in path.parts + ) + + +def test_no_word_is_left_unused(): + source = _source_outside_the_dictionaries() + unused = [ + key for key in pybreeze_english_word_dict + if not key.startswith(BUILT_OR_READ_ELSEWHERE) + and not re.search(rf"[\"']{re.escape(key)}[\"']", source) + ] + assert unused == [] From b8cc74d1358e97aaba1ff3c992a1aba90a7ea296 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 00:04:24 +0800 Subject: [PATCH 051/166] Title the automation menus' Help submenu in title case --- architecture_explore.md | 4 ++-- docs/updates/2026-09-b.md | 9 +++++++++ docs/updates/README.md | 3 ++- pybreeze/extend_multi_language/extend_english.py | 2 +- test/test_utils/test_language_parity.py | 7 +++++++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 77c7d3ab..28444028 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -186,7 +186,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 5.2 非工廠的兩個選單 -- **`test_pioneer_menu/`** — 建範本目錄(寫在 IDE 的工作目錄,已有範本先問是否取代,寫入失敗跳警告)+ `QFileDialog` 選 `.yml` / `.yaml`(副檔名清單與語法高亮共用 `syntax_keyword.TEST_PIONEER_SUFFIXES`;會驗副檔名,選錯跳 `QMessageBox`)+ HELP 子選單(`add_help_menu`,只有 GitHub:它的 readthedocs 網站沒有建出來) +- **`test_pioneer_menu/`** — 建範本目錄(寫在 IDE 的工作目錄,已有範本先問是否取代,寫入失敗跳警告)+ `QFileDialog` 選 `.yml` / `.yaml`(副檔名清單與語法高亮共用 `syntax_keyword.TEST_PIONEER_SUFFIXES`;會驗副檔名,選錯跳 `QMessageBox`)+ Help 子選單(`add_help_menu`,只有 GitHub:它的 readthedocs 網站沒有建出來) - **`prthinker_menu/`** — 審查目前檔案(先照 Run with... 的方式存檔:`save_current_file_for_run()`)/ 審查 PR(`QInputDialog` 問編號,範圍 1–1,000,000)/ 設定對話框 / Help ### 5.3 `tools/tools_menu.py` — 表格驅動的工具註冊 @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 134 個 `test_*.py`、2421 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 134 個 `test_*.py`、2422 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-b.md b/docs/updates/2026-09-b.md index d40cb59d..cb37150d 100644 --- a/docs/updates/2026-09-b.md +++ b/docs/updates/2026-09-b.md @@ -1077,3 +1077,12 @@ Continues [2026-09.md](2026-09.md), which passed the batch size limit. Index and - **Result / numbers**: 2421 tests in 134 files. The full suite at `5a5ec56` passed (2405 passed, 15 skipped). `ruff check` clean. - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_keys_used.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260924-320 · 2026-09-24 · Help, not HELP, in the automation menus · #fix #i18n + +- **What**: every automation menu (APITestka, AutoControl, ..., TestPioneer, prthinker) has a submenu of documentation links titled "HELP" in English, in capitals, beside JEditor's own "Help" menu and the title-case entries around it. Traditional Chinese already read 說明. +- **Fix**: `help_label` is "Help". +- **Tests**: `test_no_label_is_in_capitals` in `test_language_parity.py` (`TestLabelsAreTold`): no English `*_label` has a word of four or more capitals other than HTTP, JSON, MIME and SFTP. It failed on `help_label` before the change. The language parity, install menu and automation menu factory tests pass (25). +- **Result / numbers**: 2422 tests in 134 files. `ruff check` clean. +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6be7bf6e..1dc9749a 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260924-320 | 2026-09-24 | Help, not HELP, in the automation menus | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-319 | 2026-09-24 | Drop the ReEdgeGPT words no menu asks for | #cleanup #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-318 | 2026-09-24 | Say that the SSH terminal is line by line | #docs #ssh #readme | [2026-09-b](2026-09-b.md) | | U-20260924-317 | 2026-09-24 | Say what the CoT review's step selector is | #fix #ai | [2026-09-b](2026-09-b.md) | @@ -401,4 +402,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 117 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 118 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 22dc2e32..ab8dee66 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -21,7 +21,7 @@ "install_menu_label": "Install", # Normal label "run_label": "Run", - "help_label": "HELP", + "help_label": "Help", "project_label": "Project", "create_project_exists": "{path} already exists. Replace its template files and lose your changes to them?", "create_project_failed": "The project could not be created at {path}: {error}", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 48253554..02c8420a 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -99,6 +99,13 @@ def test_a_dock_is_titled_like_its_tab(self): assert EN[dock] == EN[tab] assert ZH[dock] == ZH[tab] + def test_no_label_is_in_capitals(self): + # Every automation menu's Help submenu read "HELP", beside JEditor's "Help" menu + acronyms = {"HTTP", "JSON", "MIME", "SFTP"} + shouted = {key: text for key, text in EN.items() if key.endswith("_label") + for word in re.findall(r"\b[A-Z]{4,}\b", text) if word not in acronyms} + assert shouted == {} + class TestCodeKeysAreDefined: def test_every_get_key_exists_in_dict(self): From e90c3f24e1e5be28a4ea3d39a14c22b1b75f74f8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 00:05:54 +0800 Subject: [PATCH 052/166] Start the 2026-09-c update batch --- docs/updates/2026-09-c.md | 12 ++++++++++++ docs/updates/README.md | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 docs/updates/2026-09-c.md diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md new file mode 100644 index 00000000..ecf5653c --- /dev/null +++ b/docs/updates/2026-09-c.md @@ -0,0 +1,12 @@ +# 2026-09 update log, continued again + +Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index and query commands: [README.md](README.md). New entries go at the end. + +--- + +## U-20260925-01 · 2026-09-25 · Start the 2026-09-c batch · #docs + +- **What**: `2026-09-b.md` had reached 1088 lines, past the batch size limit of about 800 (Batch rules, item 2), and the batch table counted 118 entries in it where it holds 120, each with its index row. +- **Change**: new entries go into `2026-09-c.md`, listed in the batch table; the table's count for `2026-09-b.md` is 120. +- **Files**: `docs/updates/2026-09-c.md`, `docs/updates/README.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 1dc9749a..f38bb271 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-01 | 2026-09-25 | Start the 2026-09-c batch | #docs | [2026-09-c](2026-09-c.md) | | U-20260924-320 | 2026-09-24 | Help, not HELP, in the automation menus | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-319 | 2026-09-24 | Drop the ReEdgeGPT words no menu asks for | #cleanup #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-318 | 2026-09-24 | Say that the SSH terminal is line by line | #docs #ssh #readme | [2026-09-b](2026-09-b.md) | @@ -402,4 +403,5 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | File | Period | Entries | |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | -| [2026-09-b.md](2026-09-b.md) | 2026-09 | 118 | +| [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 1 | From 61d72743a28d5d70e882051c9af8d88652ecca14 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 00:09:34 +0800 Subject: [PATCH 053/166] Use full-width punctuation in the Traditional Chinese strings --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++++ docs/updates/README.md | 3 ++- .../extend_traditional_chinese.py | 24 +++++++++---------- test/test_utils/test_language_parity.py | 10 ++++++++ 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 28444028..ef5eee10 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 134 個 `test_*.py`、2422 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 134 個 `test_*.py`、2423 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ecf5653c..45aa8723 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -10,3 +10,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: new entries go into `2026-09-c.md`, listed in the batch table; the table's count for `2026-09-b.md` is 120. - **Files**: `docs/updates/2026-09-c.md`, `docs/updates/README.md` - **Open items**: none. + +## U-20260925-02 · 2026-09-25 · Full-width punctuation in the Traditional Chinese strings · #fix #i18n + +- **What**: 42 Traditional Chinese labels end in a full-width colon (:), but twelve strings used half-width punctuation next to Chinese text: the Skill Send and CoT Code Review labels (`LLM API URL:`, `Prompt:`, `回傳結果:`, `選擇 Prompt 範本:`, `API URL:`), their error lines (`錯誤: `, `發生例外: `), the prompt editors' missing-file note `(檔案 {filename} 不存在)`, the SSH login placeholders (`主機 (例如: 192.168.0.10)`, `私鑰路徑 (.pem/.ppk)`) and the run-with suffix warning. +- **Fix**: they use : and (). File-dialog filters keep `(*.txt)`: Qt reads the patterns between those parentheses. +- **Tests**: `test_traditional_chinese_uses_full_width_punctuation` in `test_language_parity.py`: no half-width colon or opening parenthesis after a Chinese character, no colon at the end or before a `{placeholder}`, and no half-width parentheses around Chinese text, except a filter's `(*`. Run against the previous dictionary it names the twelve keys; `{host}:{port}` passes. `test_language_parity.py` passes (15). +- **Result / numbers**: 2423 tests in 134 files. `ruff check` clean. Coverage at `c9a1ecd`, measured by the full suite (2406 passed, 15 skipped): 90% of statements. +- **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f38bb271..322ada60 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-02 | 2026-09-25 | Full-width punctuation in the Traditional Chinese strings | #fix #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-01 | 2026-09-25 | Start the 2026-09-c batch | #docs | [2026-09-c](2026-09-c.md) | | U-20260924-320 | 2026-09-24 | Help, not HELP, in the automation menus | #fix #i18n | [2026-09-b](2026-09-b.md) | | U-20260924-319 | 2026-09-24 | Drop the ReEdgeGPT words no menu asks for | #cleanup #i18n | [2026-09-b](2026-09-b.md) | @@ -404,4 +405,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 1 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 2 | diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 77f1bd25..e8ea11f8 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -260,10 +260,10 @@ "ssh_login_widget_label_user": "使用者", "ssh_login_widget_label_key": "金鑰", "ssh_login_widget_label_password": "密碼", - "ssh_login_widget_placeholder_host": "主機 (例如: 192.168.0.10)", + "ssh_login_widget_placeholder_host": "主機(例如:192.168.0.10)", "ssh_login_widget_placeholder_username": "使用者名稱", "ssh_login_widget_placeholder_password": "密碼", - "ssh_login_widget_placeholder_private_key": "私鑰路徑 (.pem/.ppk)", + "ssh_login_widget_placeholder_private_key": "私鑰路徑(.pem/.ppk)", "ssh_login_widget_button_use_key_auth": "使用金鑰驗證", "ssh_login_widget_button_connect": "連線", "ssh_login_widget_button_disconnect": "斷線", @@ -300,7 +300,7 @@ "cot_prompt_editor_msgbox_file_created": "檔案 {filename} 已建立", "cot_prompt_editor_msgbox_file_saved": "檔案 {filename} 已儲存", "cot_prompt_editor_msgbox_no_file_selected": "尚未選擇檔案", - "cot_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", + "cot_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", # Skill Prompt Editor "skill_prompt_editor_window_title": "技能提示編輯器", "skill_prompt_editor_groupbox_edit_file_content": "編輯檔案內容", @@ -314,7 +314,7 @@ "skill_prompt_editor_msgbox_file_created": "檔案 {filename} 已建立", "skill_prompt_editor_msgbox_file_saved": "檔案 {filename} 已儲存", "skill_prompt_editor_msgbox_no_file_selected": "未選擇檔案", - "skill_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", + "skill_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", # Extend Menu "extend_tools_menu_tools_menu": "工具", "extend_tools_menu_tools_ssh_menu": "SSH", @@ -346,7 +346,7 @@ "extend_tools_menu_skill_prompt_send_dock_title": "Skill 提示詞傳送 GUI", # CoT code-review GUI "cot_gui_window_title": "Prompt 傳送介面", - "cot_gui_label_api_url": "API URL:", + "cot_gui_label_api_url": "API URL:", "cot_gui_placeholder_api_url": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", "cot_gui_placeholder_code_paste_area": "在這裡貼上要審查的程式碼", "cot_gui_label_step": "步驟:", @@ -358,14 +358,14 @@ "cot_gui_error_no_url": "請先輸入 API URL!", "cot_gui_error_sending": "傳送失敗:", # Skills GUI - "skills_error_status": "錯誤: {status_code}\n{text}", - "skills_exception": "發生例外: {error}", - "skills_api_url_label": "LLM API URL:", + "skills_error_status": "錯誤:{status_code}\n{text}", + "skills_exception": "發生例外:{error}", + "skills_api_url_label": "LLM API URL:", "skills_api_url_placeholder": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", - "skills_prompt_select_label": "選擇 Prompt 範本:", - "skills_prompt_label": "Prompt:", + "skills_prompt_select_label": "選擇 Prompt 範本:", + "skills_prompt_label": "Prompt:", "skills_send_button": "傳送", - "skills_response_label": "回傳結果:", + "skills_response_label": "回傳結果:", "skills_missing_input": "請輸入 API URL 和 Prompt", "skills_generating": "產生中...", "skills_switch_over_edits": "Prompt 已經修改過。要換成 {name},並捨棄這些修改嗎?", @@ -746,7 +746,7 @@ "run_folder_no_action_files": "{folder} 裡沒有動作 JSON 檔,所以沒有執行任何東西。", "run_with_menu_label": "以...執行", "run_with_save_failed": "{file} 無法存檔,所以沒有執行:{error}", - "run_with_suffix_mismatch": "目前的檔案 ({suffix}) 與預期的副檔名不符: {expected}", + "run_with_suffix_mismatch": "目前的檔案({suffix})與預期的副檔名不符:{expected}", # Plugin Browser "plugin_browser_tab_name": "外掛瀏覽器", "plugin_browser_repo_label": "儲存庫 URL:", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 02c8420a..4b910733 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -83,6 +83,16 @@ def test_traditional_chinese_uses_taiwan_terms(self): # 終端 on its own is 終端機 assert not {key: value for key, value in ZH.items() if re.search("終端(?!機)", value)} + def test_traditional_chinese_uses_full_width_punctuation(self): + # Forty-two labels ended in ":" and a few in ":"; the SSH placeholder read + # "主機 (例如: ...)". A file dialog's filter keeps "(*.txt)": Qt reads it + cjk = "[一-鿿]" + half_width = { + key: value for key, value in ZH.items() + if re.search(rf"{cjk}\s?(:|\((?!\*))|:(\s*$|\s+\{{)|\((?!\*)[^)]*{cjk}", value) + } + assert half_width == {} + class TestLabelsAreTold: def test_side_by_side_controls_are_named_apart(self): From 1df7476cc9bd9b4c72bc20d82064e315cd023e43 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 00:16:17 +0800 Subject: [PATCH 054/166] Delete the project and SFTP trees' right-click menus once closed --- CLAUDE.md | 1 + architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_file_viewer_widget.py | 1 + .../editor_main/file_tree_context_menu.py | 1 + .../test_context_menus_are_freed.py | 59 +++++++++++++++++++ 7 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_context_menus_are_freed.py diff --git a/CLAUDE.md b/CLAUDE.md index 416a9f71..de361d6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,7 @@ ruff check pybreeze/ # before committing non-trivia - Custom exceptions inherit from `ITEException`; log via `pybreeze_logger` (lazy `%s` formatting, never `print()`) - Plugin API: `register_programming_language()` / `register_natural_language()` from `je_editor.plugins` - A QAction built for a menu must be kept alive: store it on the main window or give it the menu as its parent. A menu does not own the actions added to it, so one held only by a local variable is deleted when the builder returns and its entry disappears +- A context menu built on each right-click with a parent (`QMenu(self)`) is deleted once `exec()` returns (`menu.deleteLater()`): its parent keeps it otherwise, one more per click - Delete unused code immediately — no dead imports, unreachable branches, commented-out blocks, or `_old_` prefixes - Follow PEP 8 and standard Pythonic practice; `ruff` is the arbiter diff --git a/architecture_explore.md b/architecture_explore.md index ef5eee10..c04a2465 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 134 個 `test_*.py`、2423 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2425 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 45aa8723..8713d845 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -19,3 +19,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2423 tests in 134 files. `ruff check` clean. Coverage at `c9a1ecd`, measured by the full suite (2406 passed, 15 skipped): 90% of statements. - **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-03 · 2026-09-25 · Free the project and SFTP trees' right-click menus · #fix #ui #ssh + +- **What**: the project tree's and the SFTP tree's right-click menus were built with a parent (`QMenu(tree_view)`, `QMenu(self)`) on every right-click and never deleted, so each click left one more menu, with its actions, as a child of the tree until the tree closed. +- **Fix**: `_show_context_menu` (`editor_main/file_tree_context_menu.py`) and `SSHFileTreeManager.on_context_menu` (`connect_gui/ssh/ssh_file_viewer_widget.py`) call `menu.deleteLater()` once `exec()` returns. The rule is in `CLAUDE.md` › Conventions, beside the one on keeping a menu's actions alive. The diagram scene's menu has no parent and goes with its last reference; the diagram toolbar's Align menu is built once. +- **Tests**: new `test_context_menus_are_freed.py` (2): three dismissed menus on each tree leave no `QMenu` child once deferred deletes run; both failed before the fix (3 left). Setting `exec` on PySide's `QMenu` class did not reach the call, so the test replaces the modules' `QMenu` with a subclass. The file tree and SFTP tree action tests pass (87 with the new file). +- **Result / numbers**: 2425 tests in 135 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `test/test_utils/test_context_menus_are_freed.py`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 322ada60..7aa739ec 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-03 | 2026-09-25 | Free the project and SFTP trees' right-click menus | #fix #ui #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-02 | 2026-09-25 | Full-width punctuation in the Traditional Chinese strings | #fix #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-01 | 2026-09-25 | Start the 2026-09-c batch | #docs | [2026-09-c](2026-09-c.md) | | U-20260924-320 | 2026-09-24 | Help, not HELP, in the automation menus | #fix #i18n | [2026-09-b](2026-09-b.md) | @@ -405,4 +406,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 2 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 3 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index 6692ca89..19af2dd5 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -415,6 +415,7 @@ def on_context_menu(self, pos): handlers[cancel] = self.action_cancel_transfer chosen = menu.exec(self.tree.viewport().mapToGlobal(pos)) + menu.deleteLater() # a child of this widget: kept for good otherwise, one per right-click handler = handlers.get(chosen) if handler is None: return diff --git a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py index 963d1210..fccff026 100644 --- a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py +++ b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py @@ -122,6 +122,7 @@ def _show_context_menu(pos, tree_view: QTreeView, main_window) -> None: reveal_act.setEnabled(path is not None) action = menu.exec(QCursor.pos()) + menu.deleteLater() # a child of the tree: kept for good otherwise, one per right-click if action is None: return diff --git a/test/test_utils/test_context_menus_are_freed.py b/test/test_utils/test_context_menus_are_freed.py new file mode 100644 index 00000000..58a23f49 --- /dev/null +++ b/test/test_utils/test_context_menus_are_freed.py @@ -0,0 +1,59 @@ +"""A right-click menu is deleted once it closes, not kept as a child of its tree for good.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtCore import QCoreApplication, QEvent, QPoint +from PySide6.QtWidgets import QApplication, QFileSystemModel, QMenu, QTreeView + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.connect_gui.ssh import ssh_file_viewer_widget +from pybreeze.pybreeze_ui.editor_main import file_tree_context_menu + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +class DismissedMenu(QMenu): + """A menu that closes at once, as when the user presses Escape.""" + + def exec(self, *args): + return None + + +@pytest.fixture() +def menus_dismissed(monkeypatch): + # Setting exec on PySide's QMenu itself does not reach the call + for module in (file_tree_context_menu, ssh_file_viewer_widget): + monkeypatch.setattr(module, "QMenu", DismissedMenu) + + +def _menus_left(widget) -> int: + QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) + return len(widget.findChildren(QMenu)) + + +def test_the_project_tree_menu_goes_once_closed(app, tmp_path, menus_dismissed): + tree = QTreeView() + model = QFileSystemModel() + model.setRootPath(str(tmp_path)) + tree.setModel(model) + for _ in range(3): + file_tree_context_menu._show_context_menu(QPoint(1, 1), tree, main_window=None) + assert _menus_left(tree) == 0 + tree.deleteLater() + + +def test_the_sftp_tree_menu_goes_once_closed(app, menus_dismissed): + viewer = ssh_file_viewer_widget.SSHFileTreeManager() + for _ in range(3): + viewer.on_context_menu(QPoint(1, 1)) + assert _menus_left(viewer) == 0 + viewer.deleteLater() From 490e6ef4cd39bcb3144a80a4295d5aee92d1a465 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:05:12 +0800 Subject: [PATCH 055/166] Open the project tree's menu where it was asked for --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 ++++++++ docs/updates/README.md | 3 ++- .../editor_main/file_tree_context_menu.py | 3 +-- .../test_context_menus_are_freed.py | 21 ++++++++++++++++++- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index c04a2465..6d47a3b3 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2425 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2426 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 8713d845..bc99f12b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -28,3 +28,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2425 tests in 135 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `test/test_utils/test_context_menus_are_freed.py`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-04 · 2026-09-25 · The project tree's menu opens where it was asked for · #fix #ui + +- **What**: the project tree's right-click menu opened at the mouse pointer (`QCursor.pos()`), not at the position Qt passes with `customContextMenuRequested`. With the mouse that is the same place; with the keyboard's Menu key (or Shift+F10) Qt asks for the menu at the current item, and it appeared wherever the pointer happened to be, possibly on another screen. The SFTP tree already maps the position it is given. +- **Fix**: `_show_context_menu` (`editor_main/file_tree_context_menu.py`) opens the menu at `tree_view.viewport().mapToGlobal(pos)`; the `QCursor` import went with it. +- **Tests**: `test_the_project_tree_menu_opens_where_it_was_asked_for` in `test_context_menus_are_freed.py` records where the menu is shown; it failed before the fix (the pointer's (10, 10) against the item's (8, 30)). The file tree tests pass (62 with it). +- **Result / numbers**: 2426 tests in 135 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `test/test_utils/test_context_menus_are_freed.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7aa739ec..4a245632 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-04 | 2026-09-25 | The project tree's menu opens where it was asked for | #fix #ui | [2026-09-c](2026-09-c.md) | | U-20260925-03 | 2026-09-25 | Free the project and SFTP trees' right-click menus | #fix #ui #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-02 | 2026-09-25 | Full-width punctuation in the Traditional Chinese strings | #fix #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-01 | 2026-09-25 | Start the 2026-09-c batch | #docs | [2026-09-c](2026-09-c.md) | @@ -406,4 +407,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 3 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 4 | diff --git a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py index fccff026..784e4800 100644 --- a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py +++ b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py @@ -9,7 +9,6 @@ from pathlib import Path, PureWindowsPath from PySide6.QtCore import Qt, QModelIndex -from PySide6.QtGui import QCursor from PySide6.QtWidgets import ( QTreeView, QMenu, QFileSystemModel, QInputDialog, QMessageBox, QApplication, @@ -121,7 +120,7 @@ def _show_context_menu(pos, tree_view: QTreeView, main_window) -> None: reveal_act = menu.addAction(word.get("file_tree_ctx_reveal_in_explorer")) reveal_act.setEnabled(path is not None) - action = menu.exec(QCursor.pos()) + action = menu.exec(tree_view.viewport().mapToGlobal(pos)) menu.deleteLater() # a child of the tree: kept for good otherwise, one per right-click if action is None: return diff --git a/test/test_utils/test_context_menus_are_freed.py b/test/test_utils/test_context_menus_are_freed.py index 58a23f49..62535b3a 100644 --- a/test/test_utils/test_context_menus_are_freed.py +++ b/test/test_utils/test_context_menus_are_freed.py @@ -1,4 +1,5 @@ -"""A right-click menu is deleted once it closes, not kept as a child of its tree for good.""" +"""The trees' right-click menus: deleted once they close, not kept as children of the tree +for good, and shown where they were asked for.""" from __future__ import annotations import os @@ -57,3 +58,21 @@ def test_the_sftp_tree_menu_goes_once_closed(app, menus_dismissed): viewer.on_context_menu(QPoint(1, 1)) assert _menus_left(viewer) == 0 viewer.deleteLater() + + +def test_the_project_tree_menu_opens_where_it_was_asked_for(app, tmp_path, monkeypatch): + # It opened at the mouse pointer, wherever that was, when the Menu key asked for it + shown_at = [] + + class RecordingMenu(QMenu): + def exec(self, position, *args): + shown_at.append(position) + + monkeypatch.setattr(file_tree_context_menu, "QMenu", RecordingMenu) + tree = QTreeView() + model = QFileSystemModel() + model.setRootPath(str(tmp_path)) + tree.setModel(model) + file_tree_context_menu._show_context_menu(QPoint(7, 9), tree, main_window=None) + assert shown_at == [tree.viewport().mapToGlobal(QPoint(7, 9))] + tree.deleteLater() From 53f89926858d18be3bacfb8e1e5fee9573cce67b Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:07:33 +0800 Subject: [PATCH 056/166] Delete the Mermaid import dialog once it closes --- CLAUDE.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 ++++ docs/updates/README.md | 3 +- .../diagram_editor/diagram_editor_widget.py | 6 +-- test/test_utils/test_diagram_editor_widget.py | 43 +++++++++++++++++++ 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index de361d6b..340f43ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,7 +101,7 @@ ruff check pybreeze/ # before committing non-trivia - Custom exceptions inherit from `ITEException`; log via `pybreeze_logger` (lazy `%s` formatting, never `print()`) - Plugin API: `register_programming_language()` / `register_natural_language()` from `je_editor.plugins` - A QAction built for a menu must be kept alive: store it on the main window or give it the menu as its parent. A menu does not own the actions added to it, so one held only by a local variable is deleted when the builder returns and its entry disappears -- A context menu built on each right-click with a parent (`QMenu(self)`) is deleted once `exec()` returns (`menu.deleteLater()`): its parent keeps it otherwise, one more per click +- A context menu or dialog built on each use with a parent (`QMenu(self)`, `SomeDialog(self)`) is deleted once `exec()` returns (`deleteLater()`, or `WA_DeleteOnClose` for a message box): its parent keeps it otherwise, one more per use - Delete unused code immediately — no dead imports, unreachable branches, commented-out blocks, or `_old_` prefixes - Follow PEP 8 and standard Pythonic practice; `ruff` is the arbiter diff --git a/architecture_explore.md b/architecture_explore.md index 6d47a3b3..988ec8f0 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2426 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2430 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index bc99f12b..7d9bae96 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -37,3 +37,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2426 tests in 135 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `test/test_utils/test_context_menus_are_freed.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-05 · 2026-09-25 · Free the diagram editor's Mermaid import dialog · #fix #diagram + +- **What**: Import Mermaid built its paste dialog as a child of the diagram editor (`MermaidImportDialog(self)`) on every import and never deleted it, so each import, converted or cancelled, left the dialog and the text pasted into it in memory until the editor closed. The same kind of leak as the tree menus (U-20260925-03); the message boxes built elsewhere already set `WA_DeleteOnClose`. +- **Fix**: `_import_mermaid` (`diagram_editor/diagram_editor_widget.py`) reads the answer and the text, then deletes the dialog (`deleteLater()`). The `CLAUDE.md` › Conventions rule now covers a dialog built on each use as well as a context menu. +- **Tests**: new `TestMermaidImport` in `test_diagram_editor_widget.py` (4), the first tests of the import: the pasted flowchart becomes the diagram, a cancelled import leaves the canvas alone, and three imports, converted or cancelled, leave no dialog behind. The last two (one per answer) failed before the fix (3 left). The diagram editor widget tests pass (15). +- **Result / numbers**: 2430 tests in 135 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `test/test_utils/test_diagram_editor_widget.py`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 4a245632..3430d859 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-05 | 2026-09-25 | Free the diagram editor's Mermaid import dialog | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-04 | 2026-09-25 | The project tree's menu opens where it was asked for | #fix #ui | [2026-09-c](2026-09-c.md) | | U-20260925-03 | 2026-09-25 | Free the project and SFTP trees' right-click menus | #fix #ui #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-02 | 2026-09-25 | Full-width punctuation in the Traditional Chinese strings | #fix #i18n | [2026-09-c](2026-09-c.md) | @@ -407,4 +408,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 4 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 5 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py index bbdf989c..b10bab8f 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py @@ -447,10 +447,10 @@ def _save_as_diagram(self) -> None: def _import_mermaid(self) -> None: dialog = MermaidImportDialog(self) - if dialog.exec() != QDialog.DialogCode.Accepted: - return + accepted = dialog.exec() == QDialog.DialogCode.Accepted text = dialog.get_text().strip() - if not text: + dialog.deleteLater() # a child of the editor: kept for good otherwise, one per import + if not accepted or not text: return try: data = parse_mermaid(text) diff --git a/test/test_utils/test_diagram_editor_widget.py b/test/test_utils/test_diagram_editor_widget.py index 800846d4..aeedbce1 100644 --- a/test/test_utils/test_diagram_editor_widget.py +++ b/test/test_utils/test_diagram_editor_widget.py @@ -229,3 +229,46 @@ def refuse(*_args, **_kwargs): assert warned assert target.read_bytes() == b"the previous export" assert list(tmp_path.iterdir()) == [target], "a half-written file was left behind" + + +class TestMermaidImport: + @staticmethod + def _answer(monkeypatch, text: str, accepted: bool = True) -> None: + """Stub the paste dialog with what the user would have pasted, and Convert or Cancel.""" + from PySide6.QtWidgets import QDialog + + from pybreeze.pybreeze_ui.diagram_editor import diagram_editor_widget + + def exec_(dialog): + dialog._editor.setPlainText(text) + return QDialog.DialogCode.Accepted if accepted else QDialog.DialogCode.Rejected + + monkeypatch.setattr(diagram_editor_widget.MermaidImportDialog, "exec", exec_) + + @staticmethod + def _dialogs_left(editor) -> int: + from PySide6.QtCore import QCoreApplication, QEvent + + from pybreeze.pybreeze_ui.diagram_editor.diagram_editor_widget import MermaidImportDialog + + QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) + return len(editor.findChildren(MermaidImportDialog)) + + def test_the_pasted_flowchart_becomes_the_diagram(self, editor, monkeypatch): + self._answer(monkeypatch, "flowchart TD\n A[Start] --> B[End]") + editor._import_mermaid() + assert sorted(n.text() for n in editor._scene.get_all_nodes()) == ["End", "Start"] + + def test_a_cancelled_import_leaves_the_canvas_alone(self, editor, monkeypatch): + editor._scene.load_from_dict(_A_DIAGRAM) + self._answer(monkeypatch, "flowchart TD\n A[Start] --> B[End]", accepted=False) + editor._import_mermaid() + assert [n.text() for n in editor._scene.get_all_nodes()] == ["A"] + + @pytest.mark.parametrize("accepted", [True, False]) + def test_the_dialog_goes_once_closed(self, editor, monkeypatch, accepted): + # It was a child of the editor, kept for good: one more per import + self._answer(monkeypatch, "flowchart TD\n A --> B", accepted=accepted) + for _ in range(3): + editor._import_mermaid() + assert self._dialogs_left(editor) == 0 From d5c266a04e117875efc3d32cefb0c120b2268f5f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:08:42 +0800 Subject: [PATCH 057/166] Show the Mermaid paste box in the fixed-pitch font --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++++++ docs/updates/README.md | 3 ++- .../pybreeze_ui/diagram_editor/diagram_editor_widget.py | 2 ++ test/test_utils/test_code_boxes_fixed_pitch.py | 6 ++++++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 988ec8f0..a48ed22f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2430 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2431 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 7d9bae96..c157af31 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -46,3 +46,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2430 tests in 135 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `test/test_utils/test_diagram_editor_widget.py`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-06 · 2026-09-25 · The Mermaid paste box in the fixed-pitch font · #fix #diagram #ui + +- **What**: the Import Mermaid dialog's paste box holds flowchart code, indented, but showed it in the interface's proportional font; the code boxes of the tool tabs use the fixed-pitch one (U-20260924-314). +- **Change**: `MermaidImportDialog` calls `use_fixed_pitch_font()` on its box. +- **Tests**: `test_code_boxes_fixed_pitch.py` covers the Mermaid box (the font and the box's own style sheet name the fixed-pitch family); it failed before the change. With the diagram editor widget tests, 27 pass. +- **Result / numbers**: 2431 tests in 135 files. `ruff check` clean. The README's diagram image shows the laid-out diagram, not the dialog, and is unchanged. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 3430d859..f2d8546e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-06 | 2026-09-25 | The Mermaid paste box in the fixed-pitch font | #fix #diagram #ui | [2026-09-c](2026-09-c.md) | | U-20260925-05 | 2026-09-25 | Free the diagram editor's Mermaid import dialog | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-04 | 2026-09-25 | The project tree's menu opens where it was asked for | #fix #ui | [2026-09-c](2026-09-c.md) | | U-20260925-03 | 2026-09-25 | Free the project and SFTP trees' right-click menus | #fix #ui #ssh | [2026-09-c](2026-09-c.md) | @@ -408,4 +409,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 5 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 6 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py index b10bab8f..6076dcc0 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py @@ -30,6 +30,7 @@ from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene, ImageDownloadThread, ToolMode from pybreeze.pybreeze_ui.diagram_editor.diagram_view import DiagramView from pybreeze.pybreeze_ui.error_text import error_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font from pybreeze.pybreeze_ui.thread_keeper import let_run_out from pybreeze.pybreeze_ui.plain_text import as_text from pybreeze.utils.file_process.read_capped import read_text_capped @@ -116,6 +117,7 @@ def __init__(self, parent=None): self._editor = QPlainTextEdit() self._editor.setPlaceholderText(_MERMAID_PLACEHOLDER) + use_fixed_pitch_font(self._editor) self._editor.setTabStopDistance(32) layout.addWidget(self._editor, 1) diff --git a/test/test_utils/test_code_boxes_fixed_pitch.py b/test/test_utils/test_code_boxes_fixed_pitch.py index 7fc86406..b4a94753 100644 --- a/test/test_utils/test_code_boxes_fixed_pitch.py +++ b/test/test_utils/test_code_boxes_fixed_pitch.py @@ -53,6 +53,11 @@ def _cot_code_review(): return CoTCodeReviewGUI() +def _mermaid_import(): + from pybreeze.pybreeze_ui.diagram_editor.diagram_editor_widget import MermaidImportDialog + return MermaidImportDialog() + + CODE_BOXES = [ (_diff, "left_edit"), (_diff, "right_edit"), (_diff, "output_edit"), (_curl, "input_edit"), (_curl, "output_edit"), @@ -60,6 +65,7 @@ def _cot_code_review(): (_json_format, "input_edit"), (_json_format, "output_edit"), (_ai_code_review, "code_input"), (_cot_code_review, "code_paste_area"), + (_mermaid_import, "_editor"), ] From a43af33fbee688902d5d4bd4ec609ac4bd19ab52 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:14:09 +0800 Subject: [PATCH 058/166] Read PKCS#8 private keys for SSH and SFTP --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 +- docs/updates/2026-09-c.md | 11 ++++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_key_loader.py | 52 +++++++++++++++- test/test_utils/test_ssh_security.py | 60 +++++++++++++++++++ 8 files changed, 129 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5e2838f5..0923858f 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index a8666917..48674f70 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 94fd4cdc..4a7a0091 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture_explore.md b/architecture_explore.md index a48ed22f..4812aaa5 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -305,7 +305,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | -| `ssh_key_loader.py` | 依序嘗試各種私鑰型別,回傳第一個能解析的;都不行時 `unloadable_key_reason()` 分辨是密語沒給/給錯(檔案有加密,而且給的密語解不開它;用 `cryptography` 試解)還是不支援的私鑰(例如加密的 DSA) | +| `ssh_key_loader.py` | 依序嘗試各種私鑰型別,回傳第一個能解析的;paramiko 不讀的 PKCS#8(`BEGIN PRIVATE KEY`/`BEGIN ENCRYPTED PRIVATE KEY`)由 `cryptography` 讀進來、在記憶體裡轉成 OpenSSH 格式再交給 paramiko;都不行時 `unloadable_key_reason()` 分辨是密語沒給/給錯(檔案有加密,而且給的密語解不開它;用 `cryptography` 試解)還是不支援的私鑰(例如加密的 DSA) | ### `url/ai_code_review_gui.py` @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2431 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2441 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index c157af31..ec2969af 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -55,3 +55,14 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2431 tests in 135 files. `ruff check` clean. The README's diagram image shows the laid-out diagram, not the dialog, and is unchanged. - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-07 · 2026-09-25 · SSH and SFTP log in with a PKCS#8 private key · #feature #ssh + +- **What**: a private key in PKCS#8 format (`-----BEGIN PRIVATE KEY-----`, or `-----BEGIN ENCRYPTED PRIVATE KEY-----` with a passphrase), which `openssl genpkey` and `ssh-keygen -m PKCS8` write, was refused as an unsupported key by the SSH terminal and the SFTP tree, whatever its type: paramiko's key classes read only OpenSSH's format and the traditional PEM one (`BEGIN RSA PRIVATE KEY`, `BEGIN EC PRIVATE KEY`). Measured with RSA, Ed25519 and ECDSA keys, plain and encrypted, on paramiko 4.0.0 (this repo's `.venv`) and 5.0.0: all six refused by both. +- **Change**: `load_private_key` (`connect_gui/ssh/ssh_key_loader.py`), when no key class takes the file and it starts with a PKCS#8 header, has `cryptography` read it (with the passphrase if the file is encrypted) and write it again in OpenSSH's format, in memory only, for paramiko's key classes. `unloadable_key_reason` tells a missing or wrong passphrase for an encrypted PKCS#8 file, which paramiko does not recognise as encrypted, from an unsupported key. A key type paramiko cannot use (DSA) is still unsupported. +- **Tests**: new `TestPkcs8Keys` in `test_ssh_security.py` (10): each of the three types, plain and encrypted, loads as the same key (its public key matches); a passphrase typed for a plain key is ignored, as paramiko does; a missing and a wrong passphrase are named; a DSA key, plain or encrypted, is unsupported. Eight of them failed against the previous loader; the two DSA ones pass either way. The SSH security, login form, session sharing and reentrancy tests pass (52). +- **Result / numbers**: 2441 tests in 135 files. `ruff check` clean; `C901` clean. +- **Docs**: the SSH Client paragraph of the three READMEs names the key formats; `architecture_explore.md` (`ssh_key_loader.py`, test count). +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py`, `test/test_utils/test_ssh_security.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Evidence**: [paramiko issue #1015](https://github.com/paramiko/paramiko/issues/1015) (PKCS#8 support, open). +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f2d8546e..9885bc71 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-07 | 2026-09-25 | SSH and SFTP log in with a PKCS#8 private key | #feature #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-06 | 2026-09-25 | The Mermaid paste box in the fixed-pitch font | #fix #diagram #ui | [2026-09-c](2026-09-c.md) | | U-20260925-05 | 2026-09-25 | Free the diagram editor's Mermaid import dialog | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-04 | 2026-09-25 | The project tree's menu opens where it was asked for | #fix #ui | [2026-09-c](2026-09-c.md) | @@ -409,4 +410,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 6 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 7 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py index 8c2069be..7b068cc1 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py @@ -6,6 +6,7 @@ """ from __future__ import annotations +import io import warnings from pathlib import Path @@ -27,10 +28,16 @@ PASSPHRASE_NEEDED = "ssh_key_error_passphrase_needed" PASSPHRASE_WRONG = "ssh_key_error_passphrase_wrong" +# PKCS#8, which paramiko does not read (openssl genpkey, ssh-keygen -m PKCS8) +_PKCS8_PLAIN = b"-----BEGIN PRIVATE KEY-----" +_PKCS8_ENCRYPTED = b"-----BEGIN ENCRYPTED PRIVATE KEY-----" + def load_private_key(key_path: str, password: str, *, context: str = "SSH") -> paramiko.PKey | None: """Try each supported key type against *key_path*; return the first that parses. + A PKCS#8 file, which none of them reads, goes through :func:`_load_pkcs8`. + ``password`` is treated as the passphrase (empty string → no passphrase). ``context`` is included in debug logs so SFTP vs shell failures are distinguishable. """ @@ -40,6 +47,43 @@ def load_private_key(key_path: str, password: str, *, context: str = "SSH") -> p return key_cls.from_private_key_file(key_path, passphrase) except (paramiko.SSHException, ValueError, OSError) as error: pybreeze_logger.debug("%s key type %s rejected: %s", context, key_cls.__name__, error) + return _load_pkcs8(key_path, passphrase, context) + + +def _pkcs8_data(key_path: str) -> bytes | None: + """The contents of *key_path* if it is a PKCS#8 key file, else ``None``.""" + try: + data = Path(key_path).read_bytes().lstrip() + except OSError: + return None + return data if data.startswith((_PKCS8_PLAIN, _PKCS8_ENCRYPTED)) else None + + +def _load_pkcs8(key_path: str, passphrase: str | None, context: str) -> paramiko.PKey | None: + """A PKCS#8 key file as a paramiko key, or ``None``. + + cryptography reads it and writes it again in OpenSSH's format, in memory + only, for paramiko to load. A passphrase is used only for an encrypted + file: paramiko ignores one given for a plain key, and so does this. + """ + data = _pkcs8_data(key_path) + if data is None: + return None + password = passphrase.encode("utf-8") if passphrase and data.startswith(_PKCS8_ENCRYPTED) else None + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", CryptographyDeprecationWarning) # a DSA key + key = serialization.load_pem_private_key(data, password) + openssh = key.private_bytes( + serialization.Encoding.PEM, serialization.PrivateFormat.OpenSSH, serialization.NoEncryption()) + except (ValueError, TypeError, UnsupportedAlgorithm) as error: + pybreeze_logger.debug("%s PKCS#8 key not read: %s", context, type(error).__name__) + return None + for key_cls in _KEY_CLASSES: + try: + return key_cls.from_private_key(io.StringIO(openssh.decode("ascii"))) + except (paramiko.SSHException, ValueError) as error: + pybreeze_logger.debug("%s PKCS#8 key type %s rejected: %s", context, key_cls.__name__, error) return None @@ -51,8 +95,14 @@ def unloadable_key_reason(key_path: str, password: str) -> str: unsupported, which is all the message used to say. A passphrase given is wrong only when it does not decrypt the file: an encrypted key of a type paramiko cannot load (DSA, a FIDO key) asks for one too, and with the - right one it is still unsupported. + right one it is still unsupported. An encrypted PKCS#8 file says so in + its first line, which paramiko does not read. """ + pkcs8 = _pkcs8_data(key_path) + if pkcs8 is not None and pkcs8.startswith(_PKCS8_ENCRYPTED): + if not password: + return PASSPHRASE_NEEDED + return UNSUPPORTED_KEY if _decrypts(key_path, password) else PASSPHRASE_WRONG for key_cls in _KEY_CLASSES: try: key_cls.from_private_key_file(key_path, None) diff --git a/test/test_utils/test_ssh_security.py b/test/test_utils/test_ssh_security.py index 44644b7c..75590d0c 100644 --- a/test/test_utils/test_ssh_security.py +++ b/test/test_utils/test_ssh_security.py @@ -129,6 +129,66 @@ def test_each_reason_has_a_message(self): assert english_word_dict.get(key) and traditional_chinese_word_dict.get(key) +def _pkcs8_key_file(tmp_path, kind: str, passphrase: bytes | None): + """A PKCS#8 key file (BEGIN PRIVATE KEY / BEGIN ENCRYPTED PRIVATE KEY) and its public key.""" + import warnings + + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, rsa + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # cryptography deprecates DSA + key = { + "rsa": lambda: rsa.generate_private_key(65537, 2048), + "ed25519": ed25519.Ed25519PrivateKey.generate, + "ecdsa": lambda: ec.generate_private_key(ec.SECP256R1()), + "dsa": lambda: dsa.generate_private_key(1024), + }[kind]() + public = key.public_key().public_bytes(serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH) + encryption = (serialization.BestAvailableEncryption(passphrase) if passphrase + else serialization.NoEncryption()) + path = tmp_path / f"{kind}.pem" + path.write_bytes(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, encryption)) + return str(path), public.split()[1].decode("ascii") + + +class TestPkcs8Keys: + """openssl genpkey and ssh-keygen -m PKCS8 write PKCS#8, which paramiko does not read.""" + + @pytest.mark.parametrize("kind", ["rsa", "ed25519", "ecdsa"]) + @pytest.mark.parametrize("passphrase", [None, b"right"], ids=["plain", "encrypted"]) + def test_the_key_loads(self, tmp_path, kind, passphrase): + path, public = _pkcs8_key_file(tmp_path, kind, passphrase) + loaded = load_private_key(path, passphrase.decode() if passphrase else "") + assert loaded is not None + assert loaded.get_base64() == public + + def test_a_passphrase_given_for_a_plain_key_is_ignored(self, tmp_path): + # as paramiko ignores it for a plain OpenSSH key + path, public = _pkcs8_key_file(tmp_path, "rsa", None) + assert load_private_key(path, "typed anyway").get_base64() == public + + def test_the_passphrase_is_named(self, tmp_path): + from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import ( + PASSPHRASE_NEEDED, PASSPHRASE_WRONG, unloadable_key_reason + ) + + path, _public = _pkcs8_key_file(tmp_path, "ed25519", b"right") + assert load_private_key(path, "wrong") is None + assert unloadable_key_reason(path, "wrong") == PASSPHRASE_WRONG + assert load_private_key(path, "") is None + assert unloadable_key_reason(path, "") == PASSPHRASE_NEEDED + + @pytest.mark.parametrize("passphrase", [None, b"right"], ids=["plain", "encrypted"]) + def test_a_type_paramiko_cannot_use_is_still_unsupported(self, tmp_path, passphrase): + from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import UNSUPPORTED_KEY, unloadable_key_reason + + path, _public = _pkcs8_key_file(tmp_path, "dsa", passphrase) + typed = passphrase.decode() if passphrase else "" + assert load_private_key(path, typed) is None + assert unloadable_key_reason(path, typed) == UNSUPPORTED_KEY + + class TestSha1Algorithms: def test_a_transport_given_them_offers_no_sha1(self): import socket From 6541336612fa85ab308a875f9c3e994d4a71ed99 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:17:29 +0800 Subject: [PATCH 059/166] Tell the user to export a PuTTY key as OpenSSH --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 +-- docs/updates/2026-09-c.md | 10 ++++++ docs/updates/README.md | 3 +- images/ssh_client.png | Bin 23299 -> 26098 bytes .../extend_multi_language/extend_english.py | 4 ++- .../extend_traditional_chinese.py | 4 ++- .../connect_gui/ssh/ssh_key_loader.py | 20 +++++++----- test/test_utils/test_ssh_security.py | 29 +++++++++++++++++- 11 files changed, 64 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0923858f..b7cb1ede 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included; a PuTTY `.ppk` key is exported from PuTTYgen as an OpenSSH key first, as the error message says), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 48674f70..afddc08e 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以;PuTTY 的 `.ppk` 密钥需先在 PuTTYgen 导出为 OpenSSH 密钥,错误信息会说明如何操作),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 4a7a0091..956b9237 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以;PuTTY 的 `.ppk` 金鑰要先在 PuTTYgen 匯出成 OpenSSH 金鑰,錯誤訊息會說明怎麼做),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture_explore.md b/architecture_explore.md index 4812aaa5..fd4180df 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -305,7 +305,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 | `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | -| `ssh_key_loader.py` | 依序嘗試各種私鑰型別,回傳第一個能解析的;paramiko 不讀的 PKCS#8(`BEGIN PRIVATE KEY`/`BEGIN ENCRYPTED PRIVATE KEY`)由 `cryptography` 讀進來、在記憶體裡轉成 OpenSSH 格式再交給 paramiko;都不行時 `unloadable_key_reason()` 分辨是密語沒給/給錯(檔案有加密,而且給的密語解不開它;用 `cryptography` 試解)還是不支援的私鑰(例如加密的 DSA) | +| `ssh_key_loader.py` | 依序嘗試各種私鑰型別,回傳第一個能解析的;paramiko 不讀的 PKCS#8(`BEGIN PRIVATE KEY`/`BEGIN ENCRYPTED PRIVATE KEY`)由 `cryptography` 讀進來、在記憶體裡轉成 OpenSSH 格式再交給 paramiko;都不行時 `unloadable_key_reason()` 分辨是密語沒給/給錯(檔案有加密,而且給的密語解不開它;用 `cryptography` 試解)還是不支援的私鑰(例如加密的 DSA),或是要先匯出成 OpenSSH 格式的 PuTTY `.ppk` 金鑰 | ### `url/ai_code_review_gui.py` @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2441 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2444 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ec2969af..da18ce6e 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -66,3 +66,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py`, `test/test_utils/test_ssh_security.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Evidence**: [paramiko issue #1015](https://github.com/paramiko/paramiko/issues/1015) (PKCS#8 support, open). - **Open items**: none. + +## U-20260925-08 · 2026-09-25 · Say what to do with a PuTTY key instead of offering it · #fix #ssh #i18n #readme + +- **What**: the SSH login form's key field read "Private key path (.pem/.ppk)", but neither paramiko nor cryptography reads a PuTTY `.ppk` key: one chosen there was refused as "Unsupported or invalid private key.", with nothing about converting it. +- **Fix**: `unloadable_key_reason` (`connect_gui/ssh/ssh_key_loader.py`) recognises a file starting `PuTTY-User-Key-File-` and gives `PUTTY_KEY` (`ssh_key_error_putty_key`): load it in PuTTYgen, Conversions > Export OpenSSH key, and pick the exported file (Traditional Chinese: 請在 PuTTYgen 載入它,選 Conversions > Export OpenSSH key 匯出…). The placeholder reads "Private key path (OpenSSH or PEM)" (私鑰路徑(OpenSSH 或 PEM)). The file-header check shared with the PKCS#8 path of U-20260925-07 is `_key_file_data`. +- **Tests**: new `TestPuttyKeys` in `test_ssh_security.py` (3): a `.ppk` file, with or without a passphrase typed, does not load and is reported as a PuTTY key; neither language's placeholder offers `.ppk`. `test_each_reason_has_a_message` covers the new reason. All four failed before the change. With the language parity and keys-used tests and the login form tests, 50 pass. +- **Result / numbers**: 2444 tests in 135 files. `ruff check` clean. The full suite at `d5c266a` passed (2416 passed, 15 skipped). +- **Docs**: the SSH Client paragraph of the three READMEs says a `.ppk` key is exported as OpenSSH first; `images/ssh_client.png` redone at its 1180×640 under dark_amber with the new placeholder; `architecture_explore.md` (`ssh_key_loader.py`, test count). +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ssh_security.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `images/ssh_client.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 9885bc71..3dc039e3 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-08 | 2026-09-25 | Say what to do with a PuTTY key instead of offering it | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-07 | 2026-09-25 | SSH and SFTP log in with a PKCS#8 private key | #feature #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-06 | 2026-09-25 | The Mermaid paste box in the fixed-pitch font | #fix #diagram #ui | [2026-09-c](2026-09-c.md) | | U-20260925-05 | 2026-09-25 | Free the diagram editor's Mermaid import dialog | #fix #diagram | [2026-09-c](2026-09-c.md) | @@ -410,4 +411,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 7 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 8 | diff --git a/images/ssh_client.png b/images/ssh_client.png index b8cef4e4d978e4612bbd374de979a75f69b59e6b..96441dc2ad61331e51d9fb4a57ba814564eb3df4 100644 GIT binary patch literal 26098 zcmeFZ2T)UOyEY0IP$?FaUR0XWdxuy+q$^18MUdWmQ;;qoy@N`V-lUh%J4B>}7D7jA z=nx3-Kk?&U~3+R+5#q*0Y{+-`9Oz*COPVqBPMB${RR1I7Bip zC6#e-uB77NTzY>E4}3GRW`x~bw|lASh=W7ij{U=pV@`odN(+k%9=Y5mkdtIU7DzrDk9FIDpIS9`%5yF35bQKH-;^N$@3vj6iA=*|7>j`D)d6Yt(5 zH4sW8PvoZ=eA^d#wO}9+Q9?{wke$;8?j}m^NfE2iWA1ymZ|60H)qXahWJ0^wCf23H zg)Wn{tu`kmg!w+1m2xdiRX~3U4t}|$``xQlO(Bm}MxI77D4GDAL6(clUWlf+HNxIr zS2XMf!5gc(?!=lK!eqQl{tq8MRCWBi+oGjvr)ypc?B7`@P= z(!cIn>#s43>U)_IKTxK@ror|zU#cRRSQ-qf#pAyY|K5FBtsqW~JvrXJ$+^!|qe%9;l%!;PeXMS*@v>mXsM9sjzK43F-wnlZ&(Ghc z-`qaRef>;_NRk13U74h$fPWwR1G1Si<1WplBvqS~;nha%`(a>GzQ3VJ#+WkD+l+fz zqrv1c8jZ#dT}juddYLWKM>&!@Zx!@}Ig&qgxwQ}C99zUue*$LstwAFJ<@-E+^* z(Drqu|M1`|HN@VppSS7y{=vag%PHzDEWHt$cZ<*H_|>!Zv8C;~5r?VMqxHU?7-kto zMPU^Dd;aHH1FawL6pyxaLqZ}eZDf`7OGKGFKPVCNB~b-vts; zJjLX`<*o;Q| z`8|uE5mn2%C@p>Uf{;oUDbe_D;@j1$0RhyaE)BBwZ$`R8`bj=fOMGfPJ$${GZ7eUj zX!N%F6!fRfOkMG1r%eY&TSc!psQ3Y7+H08TE-7i5_35Ewl?<9AXJOH5M#RR??^5m2 z){YgTDs6t==xEO$pZGopN3qr(AD9_yi$&`V>To+PE?7e_zL?VVeEP=zgng8q>0Rj8)#m^T1m*Nf|d+2<7qu=(CyD9iS zd-g2SQ}jbq#nWWae1kHL{AqP( zJvco*{V_`PVtJ1<*UlN%BzDv)Cw6v}`))X=vP&SZ?&S4r?7`dH9@*)>TNz4wi~4j0 zU%!J1mBsW<7GZmfl2Z1f$@|^UFA@_I!iCz<8C|y8Z!#nvI*7C{U+(|n<u6yeG%;pgl#xqY!G$(D0t9sJfiOYev_g3Y-&Yz zQJ(y#XD8UNKYzZZOiX>h8S*|Wt$w`DT08r2sjno*D4D6zLq{Qvm-KFUNJ!52nqYQq z-80aL4+7R&By@B^=84o2sj^1VmKjH203=PB48Iyuizn;lo#Pfa<%?Rvi_8{s2C~zS zc6J`HvBefu=pfm)kQx0wF%yy>gB<52b3E(8bZ>2IL%U4gyh}zFlbZU>V(m61zr4X- zd{JEJf{ypbOL?Py8r^Gzgbad$)yk;6;Fs3>+Y3siCaVr`8kknqY#dnzJ*QwKXReRR z@K@FEQhUpVn$;1jyr#INc0+d#w-8|2 z-J$MJ9ICD`T4I^3wDw2Xn6-5(P(yrD^~61ub0g?Z4>wnG^?f?HKFO}cf*>=ayy==} znRpB4iE8NCm4fhYu5;G9v~#&?U-)<*pQk)#)8FT%Q}9Sw9?+@Y(#7bDY z-rdJR`En(Jz zVB+4mK_Exu3LETs@aWO^U-qd5k<-&UU^WMbg|i{v z>P-0L;eM=iRI$BM%ci@nQmF?|;LslQzKNMQ6RLZ9*~rL9qtTXO< ztvh%8N=i5d^&p|_+SR+~_%Rbr#`w?iZnj&9r)6jssWoBNESJxYNx)5hRV`4HKRcnq zer05A{BCkT^zZS0Q*@OX42`I*f{CZmo#!v~U<90vtqYM7?W%~nJLKfu&~)Fx1by$c zbARbl!v#}4FPlp^-gty>=jItPD~&Rr>Q~M!!j2|yx1sm^kLrE5>dhMJ*(D^1_4W1D z3U#Cm4Icy%)0p;+75y+DzR||Tr4SPwY`5VeTA)^9IF?$w;?*ePy!HxAMP?=TH3}+{ zu#6140(fEe_wT`3X`(%See_IBbt9+_NonKCNsW3N@qmDUjj8I4%%#FiCkbU0mFpl~ z>P`t9A0Pj0aOXApmf2^uvarxr`rx@ng$s?aa2y@(UGiJ+2Ta>mopw87^ZJu{6Am}0 zYI6kJ-jPE{^7d3HJ?zDE^mH3n{Mq$+!lTM6{5(6`Ex2YwhZEpVcaTczjajqx~W zWkKYERc%_SWj<5*7=$EKX`^plmR=#HrCeK{+lF|AG@A5qXPgTWXSelmxZBa0%-g2s z?GdJ#xyQ)8ot?Q&K|SN)Wp+YJflwuAVrzSXJ_FbN=Cf1AuAk>&24@UPXuY=%HQ9af zQR(R)+E#H4(7kbY@7=Ti{nHTS$m;||9bBnidk8)k2BtYXp_lUVtuR<*bMr8&CuczW z5gQwWFk=7)<5gud^}^>VF0)cP1IQU5e-}9^K7ROcUO_KI`DodaQ+<4_(UR;E=Ca!< z1U2U*qFL{AS>Km8JjJFVf+$5`!A|HMxIa->)G{UZEz&A2#d%=9n9SO}t4-nrzx-euag*wo*FZ7PWJt z1M>X+{kM;fR+Mf|t6S86Y5qm;ia9&$=^1!@FRXfz>@Jt>xzhgDgnvQF+Te`$X-`kj z13^JpWB7onar^dH6BCnCpSDhx16$z(B1HKTtDO*aRyGaeV{_)~J39=y)CMEnQEZx3 zsaaWRAZGId-~yJZEPKtClSrAC99cr48Y^*7<}dp^l8IUiwR+y!QeCAJQM4nI{*-}=^m5J zYW>)=vtUuo9Y*+U|9s$aSQFaX`i$1?_v_ASd#Z)5@CQrHPiNfLX^3grM8(&QK6~?| z04xj6mv<1qySoeeJ@~z8b~JgPPE^}MW?B3YKI?Qc;tp5r?!65prjXRrqa`3^ zFLpo*3m1RWefg5BD>B0lVw;E;b^G@CX44iwUqn`Rl*25J0wq6-s$FkSPYVndfv|Cf z20dtL$x@x0TJ7DRTTu^+VOCOer<0Pjh^Z*qV-(X}kV|yCpj6cqNC_9z^Gy2m$ynZP z87*5K;_BXev<8oS2=a6#tLzF1fJ|F{D$cae0Wu^Hd zpFbPe)UM(g8GQpFPbgI}H8)WAvQQ|^$B!RlC#u2X+$Fd%jTE2}@gjc@4xV3Gk#6)b zB&6W`-26oS+b^yMxFB#Ut3-l247-j1d}IozQh~7aB5ZqHoY#(r^52H-O@iO(gIP?$ zZS_dI%7&hk^JZ8_>la)&m|d9N^;Vva;%N9m`zqgfttA;ifXzr`MtAo$Ek4`MCKZCu zQ&s}tZNz{n{lkZT$2CF`=_jV8Sr;)A698HFpH;sb=lXz0v{nHTuEnOjn|Al626YN` zBqbI1oWe`YK{4aocuXxN)gg9pbisPC6qySUVt*kUVcQoyjn&7$=ML8FF~`><(R!0Z zPQH zboDBF8mv1o+~feOG7C>s61Qv!@BJPr6F>Sy*VR4OHs0`-Rn&EhW%!~24Y_@9^+X$l z(wGdN^8k?1SUO(6rkhQ+xP6aeRqv;+q@*kuM@}Vx`bB@(!Yb(ld98jMRN2lv`BkOB zU_I4dxH>FWB{O=Dl(gJw;sz~x?{d4OG|4Oq_C2Z%^sYFRP2)E=k~Jyb_d@#18UGzhOgRhm8ouSci{hpq|x#bia15AymIVk#gO$V08cNS>(#Fd0~RHP$z#F+?5$<1(YnttpIt#{_DKiXjA z;E2yncS|MSoQg_H>gmexX{pZL8gp4V$Ru}Np^^M3T?!FSg3lZaT8g10{Sd#9`$*?AoohStk`LF7Ka zBmE9p%WC`pp z&f`-wYd!b@w8~NKBi*k#VUajBlFj4CKiS{vI6v#jq874y&{|vxu1U`CzyJn7#4MWi zp+nxDn%^7|3$;$epBh-i22^l9RJOV+jZN=rFfL*7Y=*P^@LBy_V)WIkaVKeSS=D|P zbd8P_s7I18D{^^mH7Iv5z3-5V*9*3@Yy|zWv+ftbA`tQyLG~v4g%I}bR0p4f6xnQM!?GI><49@sBI5^&A62v4uWbFzp$%qhp_G}i zyZMcMcX4-j_xi*#5;9=tcM$=qpur7te`vI*zW0XY8!snym(iOc^<0FRcn<;V7BwmV z_g+a5eRv{hTp@!(E~%NT6#V2-plu^+EPvXl^P-A*vz1Rlp2US=Gk?_%Tgt}&`ZkBX zY(i9Os*W--?WOyo%+-OJSn_M~A2d+^AmY?QLoDcZ%A0m*$_WT_8&mqA$f7nI9#ejBoPf+rI zDp|dib!p~gSp~Glf-@Y;B1VNpzsSdSuTI?`Rxv80C%-mK3d=(CrDTE zl}BZO`$G`3h-O{S_dirqyBuGujK#-)a*5xU0rH$N>+l;jZQD}E538Ja`1O58jqS|=&_eycsCb8riZqwUH-mV2h?l6?rTHboIBur zc}z^jLl5pf)~t}}$Vq;^+hn;}xck`l4q$p5ic|CQ-`GW4mpgB+NjV!k#>pTM*P+M=A+ki?re`)4L&=Z#*rAE<8rdb!2H0yY|K znOUoet8U%3^9g&hyqM%Aj5Ha-Lp)Rm?0C#4*lf zYd8;%lSV#~`Uk%pCD(qHRR|$6-r8%;bhBaOSmDeI{plt+cj-I-giBx$|Gc6I*8Ga* z?%p++`#FScszX;bF1suB{B!SGu2kDnIS66l18R*y-hf!}_1X5pU@-Xy&IRA{xhJjE z0tsn9LZKH|Bji~!0gZSY@#U#P&;xL*^ftBV7n4=yD#M0gvTAbt{x(yz7VA2^Xe3*QweVA>VA(1L%?+1_)&SbTvT+rd>HDY2W)^!Fd0UMLk z2ua%@9a*{Byaq8ir#R`FtWzUtN7=x2Rj<}!s`Hui%$R?g9U!otE0 z`@QXypSD_)k9)(ckWt__qoTd)QX{7S^~Bc*s`Y8v|^~DYxEPPaB4}@IcdS@ zd|6{%%$g%jCgbK(p0?n}ni>tjw=DV$npXU(Q-NRq!G=3*1&Pnk)t7w5kYtF^Bi%7S~EtWdZN!Nlj zUy7UV4shbB`0#KkxS^E<@LVc+mMQSH4-=iNYz%jiUWsMx2CevtI8EL|L7hXSC*rT< zK&rGG-Mz`5n0ph?AzFJ|r zHz%Euuc3T3ZXgA$D4zl#AlyM+zfQcoDX70RWe(99>B!-w2z;tF`Pv0F@!TJe=-OmO zP?_8-Fs$MX|>ri-ZZtmYP zYj&_>`(reED9hgX&YgL*&E(CIBJG6pvy)qhFStobNo-mTv7pYNUD$wE3xX5KXK)h) zwY@f^qYu2v$jqD|6_$Wg{94j_s%oxJC4kmWA)wxIpQcc+BEHI6>oS&V5wM!R_5|D# zAUV6x5Zs@2s;mHtgdyOAe=&a6NyqI}Zt8vt%h4};loTAz&DoN~QI zPCm=s8F~nL^yoA8t_|jlL|nImGY~Ku6+63YxVX4rqP~3n`ZjhQE!0_-Fg|*8D`lX| z_!Rx=(hVY_Y+s}V-@8{C8ynPPMfxy6)ViiJc1;`&n|+RodTm2zMwj{KVXcyk@RRx~ zYuS6L$$aL|3=MC0baZU3fSQ?mIf4U_-F4FTAuk7n9Qqe?N12}DaxJp3a*7+g?XO@8 zkjkDq+u-^V7RM|-)!h>$+((p7X}y$luBQCU;M~Grai9pZBtS*fYB+z_U!h(e&m}-D zoUPbTg%>W8kfv30zHS!uf{1NH*u zZH(EiAxmx|eR%_x!hDu9Wl+BwG*w)+b#&M+yiXP4IZu*GBrXyoXeKi%pv)I1LNQ2R zc;lghl()Rsl2kfo-FORmiWm^w9f*bX?!&>cO~;GR)i^YpT~#0}YXxW9B>&J!1flCmKDKk|g9Np%@DmD5mZ+poU&pSrIlhHO0*&+%f#> zoU}U%|KD_|@^a;!`2}IS?b*D|w(Y$=`laR;sv#KpqnSCZqS z!je;1$Ogy>Zm;oM$$U1SA|r1C20{L8S-ZT#ad1LU6)+F$FTxk!d5FY7&SS5Jkq#A@ zDS`7Nrcxyad001czZs9jv}Z9;3R*}S7<@Ucb+Mi-b?+m!5=c3%{U=LYw0j;B?3$g` zvaT=e?ouA3WJaY?L@L5p8Efvls>00awNaO1TRlc(m5GZnAcfbF`UY=eHIYm-}{LIf?ir4|J}A8+cE9Kmi9#oKZR z+rAP==k+CAm}eD8>)|4-vPQPRpd4S?^1QOXQQr_-@x51hw%G&SP3WR^u@yw0UD&~N zjS-R1q+JU5K8oN;+efz<&#pLnh0_zcVj6BjyYMD`sKw)(7pp#`=VA$@11Uo}&a63J zhqvW?&Z5-yJR+K#-xD0YaJM_;?@^gCsGg$Wal0Qz%84bI?2qkOp5qZ+*&eTgFa{hS z3*lhaM;@^7M0bW$*3t!y6=7Ab0;jZr!9mmZ(ytp6^^uW~v()yiw2vRJsIPm1Dj?2L z^Ij^T8`1DR*!FHg$*}MCH4qy6!(a+H6^L)q|L_7d17V;V=@zb#QqA>l{E@_&(G3!P zpQz^dp)v2L&W}9-GuKQX33+O0__a%7`TUsV?DW9icYABNfEjXzvCKQaggHacLT&dB zs)v6Iz9UB}Wpv2F&PWPI_cHQe@4vF?G<>{h5Dc+Dnnw9ZJa~|fM2ZAL&tb@#(_+z( z@NnT3Pm?l@bFT|>qxsxbwtTzJCtK>HVM^y{oB zR#z#6sELmp@o&``qeA9L5agnC9I+W@WI0Rx}1&` zTfGa4qZ$SJ=hdrMFCAoWfX~aJObYowI2q$hbYwsZ0QDpj1d>tu^QZEr^V-N)MH+}= zdOu!sNdz6>9>qhyOZxbHC_wml+}_j|e#?Ok1SAk>7$6#$xg@#ohztBJZepw87+KSl|Oo;tILynsby zzRnEXssg(2Au}`39oH+@MhO6+;s<8KxR>lr<$?bGEWO53hoh&G`TDQk2aJX5!aEW8 zeWZ39O+Rb1*C{i6TC*Gp;1`@4X3DP?CnW@tCOa?Ghw>feSVMZ4q&j|k;=s68H|{eNy5tb2^njcV6vFlP>)i9A}k^7TEOeZqOFXN zCuCdVI{-k*?wKmA!gB>h?~sTuze?YQC|S!T6n)W7V8y*gR3z6&I9R-(}$w-@9KH3Qz&cD7Uoqv-jy?jQxg2Wm6D9^TG+Eu_8fj zAKK==xL4$COgE-nD4x7GA2!4JdC)s}1^?>K41NqZp}(-BugS*msnzK`dbxOZ&S7Jx%py(<}d!dvVCHIay^0-$FRmmXnk;?WpBRgwYVcKss~w^gK?? z>FJ+vbl;c*%+x)1z0=dNDHI@>y8x3XWdATG@cIt5h|~B2YT9|_-Hou&mr5|ZlA8nkgsFF)w#q;YijSx{?wZ4ko;7`JH>c13tdH zxx?T0n_`9b8hHpWhgI%ZG15oM-jELnkOD06hKpz3w-_kY^69VEZ_gsRJti-r4NvZM zEZPZa0M!*pRg5ew(ObIp4hux^y^aJGJCSh5s^=5Nxh6x~6<6`_qP=p(;vjzz(1vPNr0ZO2 zqx<{W;)^t%s;R|yRRR7RaLaed$mFo`vdSAj(ER6jL;BXX_1fHZ)g=)szz!?AtXfM+ zF3vlF6mo-rzy@rh$=$H~aZhC!vI=QBrvg#&cdFb?H6V@YMgL}q!b zg7-9QEr?>6eEYlTFbNRMmTA+;iFV1xRL3K8I7c864In~{R|a+8TkdxQLXX?>PzT(F z5m3!@{Z@m8g=@F!LsUCCLBa<9076PPA|R@@@(nm1mN~B2oE&b3BVZ-dt4>Rxz+vL# zERj7`)?}1o^t})Peh-KFc6)G_A(3Im0F7>K&KntHRWxSi>gn66jM$l5Ax)o_r&*B$ z7D9;UhmB0>;iQW_POK*bI3hld66}rZjoLn|P%v;| zw{sAh8<&akH!p4?&Z&LDClx?JxMj06c$+l6wkzm41+jx-TC2bopZl6_?&(1}$9#%z zeBqpvK<==nA@S1Vi@lL}KpA!_q^wRZ$(|f;bsX`U&O3>$4%aiQ^O^UT-($rI{`6@U ztE#Q69&OKleZIe8A9rdmqeyz2b};J4kFau$`Ny{ZnQ}I_vZ8L7Vo1)wO!wt=+m&xr ze6yfPGp1w$E&yiWIY96E;^n)f4fG5=;NrR!*72=l9=HV@DlKGnP1Q&P4PS8b1SgtcsOVTWSDrC2P-$h>^h)l~nR6oI0 zKyP7Vg}TnrM}+6$8*_aYRsb_FewU6OaR5~YaY6)y5s(P3AH(yWw*_9u653;h?A~EZ z0A{RtYCL4$d=yT}^fS*`8n9KRQ=31mhm!$N;HfCbLiPVdjLYoxPAdW4h+iePwmdu$ z_8Z4L4%OF*iStJu3Ei$OjmY>&r%utYX}aA*4G&t`vhFQT+MgsVuEMu*NDG29*3llV zTYZf{RC>Rs+uwTE!X+=T<&>r(H)c=iQW>X33(;Xfup)+y?-ebx2iY`^BcgP5S zOL!SD0b%E9T$*T^6jZvUFsa#CwxdxKUk~tOkq1#5Flj7 zJ_W&BSUS(NZO&<4j6ZHrh3bieEYCm+KRuBC0r3vH<{=BuB}hHu?CLAk;)=XHX)Z#4 z4t1AKo9bb}T|A_Jn2!nvB&cltaj^>&pyLMs&CEtYAW!6-_<;P3WsVeSruO>$0wkpc zXR(zlDAzC>E+Ep_-z5XHWnHjlE7AGiNkN=Pm7$?w$6mc}fWFM-vcHM5vSB>BR(Xtn z%xl&C#Q`pKgOIR!m9M*J05A@pwQU-3?G*BalhYM}nsym~AE23FZJWBUZw%Ri23s>u zUZRjIBW(9M#I|7os^=C6){f6!lWt+O%NE1JIQP+yZhCWbtMQtUDEgqiE+o>g!>DD| z=??V3*3GhVZqRYfv)ZU`Nv(TToRJVt$oh;~G zy!OJL0!r;MZ1T8ESD?vj zETQ`qf&Q75!+v|7->{9kVeb~f`W;$cA}khcT_@cZ zZwhQt&xM-pr4LJGN4vo=*NLilHW}&8ZBs2z>pqZ1Mp`E4QunHyujCOKOzg%H(upKu zYm7H<`hm=4K2a{*((-hR(+{NoC%`UngcMD0Zhj)-@(wm2h-kjaYZ@QoIJ`Un%Aih! zJN}0cANEmGDa!-Nsz|D(54jBwz|zNB9)C9DPJ6>OCabNn`kQfbPM5&@g)XJfF)4k( zoq!AHKWP5o4}2hYP!}-GfucC!xH7aK%JhlG5uaUKfVt7HzAZHPktTpltZgu6;ZzvX_jsV z7L(Ju2P3EW5n=t|#@y<4SX}6BgPB5<`+2r{FHNAH5%7G9o%Qc!R{-zN_s5w%0aoNPvD&g13!KMOmepo8ew3)Hgs&1ds$>)@oVh*Ak*nwLOPiHuTGsjCc z@9PHt?S-Z9-t|@O?d>(Ww?kiQet5cs?8!F~&mJ@m^;Nl0SYY!y!>o?pNX4V>;YzA# zU2LQyOm1(6!%j;*>U+E|9G3VIQyOkZ*Xg2_<yTNKrxE|#9R;gQmM4)?Sk6u*{(Vx|&)8r1A*;-|RQ3)l>|w|c{;e*Yx&^16?{ z<|P0(7<9;#Z!n|8yyDiESm--p@MpeQU|_qFBcZ*sfXwId_21o%<#q4+9=tWd8p9@r`V zhywTI6V(2QL`C0#hGe1JvTAfRLJ}Dh?-JXS;iWeG)lK*9kwS>d5@5et+uCZkCN0rx z>m`VgwIqxDL`MZ^lZoluTInYDZ+^o8WYpB{z}uO`ZK+n<6%t&d^V=^b6>>(zl7kf; z@>8mBC;|Y1=;#MJgXUvf~bmu_kO$kJj``wLaC(}Uj7@bb?wuB+^N?o~*H7yS`J zJVNmNj1G7s#+-!oR8x+Mn5?INB!#C4cuq3`{@NG1XTvY$lss$>7*&Y6#Z+?WjzhT> zI`nU8QZg%+Kp?vqlo}-vBYN%$lc||K&yev)1nQM#+rOz4Nv&&+ZRl86q@TA*{beHV zkn{Bq{Y#KrE_&=tX7zFS8xdY)6E0A>e%9@v!<~A2c-{Ajo_7E>?=QJat{bRg9}0f| zc-I!-b@lIWZ`Y2$8?yVyhyU7x8EbwVlpM?f4E9lNnxLrdNh}6&^lk2lC`g z7+--fO9-JeVn74sjgA!z34ZBZ@W?D5NY+`C=u`ZWKQ4Zo6qlCf;%)R5s%j;>{O#A_ z;Ak(27QC8sV&(WhqUIY-5$E;1LRrA;b7(h;Xt?ch)3Nc>bieJtLYw^CWP&L(=Q+0N zoMlp7O!D7X?}HyoTFA;$S|Rb9BuM=(KPZimRT^a!A2#VAb#u)BY6KL=m+YCh6|n*& z_BF3sCztlWRmdCvX2)O3_n&?0|EX*lDJZ%=+S2-cMnHtmzxx$JSp&$Fap~se=4AaE z^CqiIFhSnYT7Ai^!|%OlePEp;k;}CaZ**W>9c}4NiJbT;O$6lfwkx#hzrl|)r(M!c zfvqdoG3^2<$AcY~^r=8;Hp2a-lsGNXBIA+M1n#!>jnu!DfLSrkUmc;rFE^DZ_Mkv* ztaY;{$|WAqxfsPK2G61Bd*l?nzmf|>Ee&DY;gYw3ZO5WcgPI+SX7zJAA<$z%(R;PI z&Ph&NWViFYSsc&FZr>IrLu4yryA%8R|L{?q+WR8T8}B0{l@x;PUmrW|o=$r$9PrZw z2EVKvn$YKPmk_8~5uu{@)VxMeWjFi1M;$^Tul=s3x=f3GntP--{EU6%X2oZZ{>2o1 z!W@i=LGn+&!B!AeKd0C_w8aqf@aRI15~i%CMaFwoF6_%!K5MR8_YON-ag&CvXfTT} zwZ*ZsFP)!{H8N#htZvUxF+X&;%_2;9IgyFF&#!}uXy$wj6lGVATYepRPh?m)EW{K$ z8iidUxH)5#t-?znAwVGWm|>=eF5gr;l1)b=7v4uMl@?HY~vTGJC^C?9Fo@F0J)=@p4^0-UjeZAK8HZOCyKT!2-VnTZM^jjuxfy@!6Twm9ex0?1<~sk84mMsBR0bN z$=<=cSbqZ-;Wgs-bjW?feJ%o(<>yum(<4aPHSQP)1|jn_O6Ci+<>tli+*pg6M3Qmr znxla_YvW`J8^Y?%cK*r?6C;t{MgBg1cyIg30B7!LJ(?1ZA5w7&I*~dntHn~kZqV1a zuA{RiO6|qX%H5rfUn}jb^K4h}o68lLNF?kkizaN|zM&)7A!56GHxmrpRnuf9;I3a> z^_A!v;gp!Gvy|QjY463tVP<=XDk8PhdX{qQRj%1uo7)nvsO+3Iy zn2H^=FZ_5n^J!#+z5#t$&!$y=_dQ_mFsKVmfQ$8%9mD1$I`3V8uVLALnV}W}d>eZc zMQ!K-zz3pBpC_IC;W*wcGO|8ohL<0(4}Er+z0cXm6NODHpWg0p?p*_RGX0F1{hBqF zPJ|=0%V+Y3O$Rc=!w(byYQ=ib4Gf+TVVuDNj~KIymeUH#DYkW2x8mnkT$-J5w?jfx z<&T>~MsK8P*AVYrU6;QRs)IaVUQS_~d1p|m^?Q1EAL0*Wu1g#mw2Z*tkV>Z{eErVU}i#}oJINjzLixpoYxF4t*jHViG!g(&)Hz5OlnKsg-P zegRZ>+Wb{3uMOO?7ar#ndOiovCpT$Z0>e|>cWSTW9G}AHkR{OGZ;WyjLi<{J7e}IE zNBzT+g74n{=ghEowbf86WLn_g$_qQ=%RpzFvTf)DUT<!{dJ~Ng)~oGl}zf-sVJRJY7431BOk9XSWkOCM<;o|tunu3Y}o3S za=KFyqpQL;c&{KhIM#V1YaOG>nPsJIgZs&|rA6Pi;#YM{oKM;V)~>}-&eVQAn>El; z1HkR^G>mkhPYh)z1dQ8!wqvnr^{#tJ89FZO6GEIi*UeBvN-xYR8K8=RNy6QEKIIra zu{u*pfP0SHaF%goCz4jE3;j?6P3i>FYgYjY^3rTkHIh+~GqQ4B&~9FmiHKx=qQXo+ z!1}tq{eQ?`3#1gaY7CA6di&m?${U@JK=1=J-6unGFpXn%>yLDrUG*dv=QI6cbbjsL z(2;R_X|TGI(y3W``pjr|wOxFp@Foi*-TiV0SqQ{9q@}X4a9U!d_atjK2IFX5VV~Qh zf=W-H{~UL@&C_mBgXlRO3rKfroZ|<`=u1B*{$mbHmsv7j)5PQouO(uJOf1& zd7unjZAmFW2reK+{8H4W&i6)WzibD1a`UY&9Pa!}r?zs#eHTPFQ>A@uz>P@f0^mNT zhnI+rw5B{j6@ER+!lYv2x4o*XfYtb$oFY99ay0s?*hvEOs^B}JisgRe`2tSEiiww^ z9WGfA-)LBn7fl|V8+jw*ic(!$sYm5{e#&M! zqgH_3=i&MGW*Qj!sM2u3ZLYuX0f>^FO6z#vobFs=tTXpwy9Gv^0dw>wae0KO?6&xi zi)G1)D?YyYJ<=88zVu7xDaR+qix8oy$8K3DOd-@{kK`ykD z;)LT?zBSX_15(=h(N5&nkDYEht7+j@{yM`wS{3ZQgK%U%{rxtl_=OUVPa=0gzrfHC z(eb-q^-gQ!xRClZ;P0pI=pYYlnRKRV{>WX8lm*syM4eubHi`aM#WWG@asW!m7SvS? z)XZ2vP>OBc2O!gnq1@oM26d<(jV0bUUBc#*DY&f8Y+dDdeMk{_;VZLVy z9@<#gYVZg3_{q_x!k+2Cpr|PTSW!76J&dTl3v$dVDfusiEb#t3HfMtQZYr|&mI7~aMXTVleF@}b_^8PJ|LcY-AQ}nox^&g3v(Wd33h8w30AkGj&MQ0H}2_(K*!LzF+3^BXjxTERdBUkP|Yj zX#&e^n@dOT9+O;Pg=Sp0?YS=ZwU92f{ozd$Q`6nSdQ^qM&>fKK($xC_(~nt=f^N4j zL~_}V9W5>`b018ZZ@pGYH%gLlB*m85LqkK;r_Il4Pu(|4ly>%!=_dU&)ny(BS}t3Y z&U!nc!8-YDX>|wty~i5|8ON7?y33ad){y`YXLIe~rpQrP@Q9)aG%UE=Hv6&jAF%!h zum6`|`Tt`a|8Eb_V-?CaOU40(Ov7k4@W6hF-gECkH;Vmfl0=;F1$dY^H4J`3F~z0! z{1km9MZygjPUruwUQ#8X3DPS*AX;RXulNGLsN>F#nlx{`5XH~YAYj^rG_6+JfFcer zsd=CAMZn)Tx+3z|S=mJd4E+Q}WM%v}Pn+Rg&+P7B85H6_2R`C41?8Z#sa;nNvLwNbp5u4K{4=uR#5kC(KQNY1@k}(1oGzt3J z#^W(x$UTe`DupX*Tn=ve#L`MLa%TY8)MAC9=-O z(#opewdnwb&gIliL=%uYA$(5EbC*9ZAU`X4pYXcR1(4N+mwn4-)UTNhqx$5#Oa<$X zRZ$!iY(f`_?fy1KpQvB3KTk=iN37H`%I!{E9H0{nQB~}8=tx;WbYIkAaa}LvDD||MwhO(O zE0H!frnY_)JL=ZMEstgNjwb1O7Mf0Z_CDYXdSu9EN7L&cP5!8?T>1&8j$U=0p0GYm z)OG36Z(LJ#-!JQ`vnePkfxV82l&@45yi503 zi>x(WtJJLyr{;dBe=>hHiOcXpZ2xDQOmvK**n_?V_P!reHmEw}?gTzDl7)lDqAQ95+G0lg@Q zVSqN8S}-X|IQVbg6yLXt5Isz^sixsso&8a)lVq0RUA6JCS_Pb|4`p-gJt(plp7(}@ zLcmHxQEcv5=e&Sbxkyd0#fDxU|8n#Y?u95JEVt7ipFn7|t9D+9G~? zN2H4hyCNNJx6#1Zd1F#&E6E2 z6cg!n{5(l$=L?^JKu%>PKiT;n)U+p>u~EVWCf>F3LnU_+!htpfk3lsinG%9{^Q3$r zLs`9GbT=~t!=QhRS#j8IA*ok{Ygvw$8e&i6wWNQ}4qonyQPPh4ucen0peN37Z!K+Y zUNA`=_lKmH){uHJWyY=p$2$vqX<_HKijd6_n+5L;db+K{N|jG1#YIKz(AwSnhSkCf z9;bJX9GDf~iCGCtc(qo+5_%;Art6A~q4M!vz?*`XpNkiaTA(Kdo03SBgd}Xe_fmL_ z+JpORQ0F>3>qwK;U)faNIw~qEGeI_~*QTB`Nqm2HGrr9uE}zmHmI?qc>d%NNn?;&Hf}rC`e3^qxJ_)rX~taw4IN(VuAf zS?Y^jWmmO=7J|*41hcb6> zUK~%bX*V^jA37or2C1D#qH0|`KiNb=SC_SS2VEQ39ajbiFjol#rW(Hfm~dQPoOVr% z0poVg3AtJtZyYJiRK%)oYJZcg6=~l^<2WdaOwTyqSOFt6Q?i4N_$fSJ{D${nkc>W# zi?=gp#g5So(hBLRo@V{ONmHU11Chu?POo#v3HOz>I)qPGn(OjL#bCcL;>S*A$`~|o zAb+&L@tpah=_hUJ<1wghy7ytN*TFinJIPjz-YnHeD}U5tc`z-()@Oan+|qJt_B^O* z`7@!l%^?1atDeMm(_WqR>2fu6*x8!zPZ-sI!$c!dt7i0NEdCy%Y3rTu*Y;AJsnnAL zpSrhuKMhls+`CXm`3yY`%(PQ%FffvY)Y%Xv!bHnIX{^-w*8gERLaA5$VV#OnzxXRQ zO^$~OxBgE7IRE1i*S{L{KM@E2YgGOs$3zc2bk51RMhIt^^9cq+(jRw=-b8~7srSx_3VRve8Ff?^hD ziY0dh*Xr$-=d@HCE;uIT&oQ2nMJA{+4oXm#TGdm}T`7&T{|Ks>(jrIf=Kn-7^Z!zf z^sm$Yw@S=^b>F|b@4rh^{Qq(eaXoQz`oS!-woX;GEsu?@J(I9omJpTL`$D*cw=C6) zWTQXPK=hnI3_bz8*8}6ONTuC*v(P(U4{^!wYb9{P0F{*u_^0~K3RHYLh^wXH=9*1~ z;~IV>%{XQGm5gCz{YrbKbLNwV!I{|Tt+4j{9V|Jn;8(ekrdo9sDHS~T_Iq3tRZBUw z$||c>b8PGuhW|67gOC3iMfexHctgm-h|!E%HzVYfF;f~O5GR3x4mrytw4}pm5lxan z#n`%c^@{T(<-tnjdSq`_8#5V2QRUnJRouCTC7o}3oc28FX`6Xww`VHN9LJoQU7=Hs z87g|pDYy5@>xc?@MMbnQB}24wr_-*qG<7sDrKx#A#tWdJSmT5e3VA_P6s82x02viQ z1Y!T#&v|*?>{sUn4=;Ye^Jno}>%Z3LyMBv{_hLaNHOtS3$;!WiKsTzaox2=_I@(T} zLi3_Rq$6Sz=EqV?_=Kq$R0uI9AIRmefLg;)DN#MH+~VTUkhOe(X_6zdT3>Y1JqoH# zT7oI-{ustYR_pOdT`pJ>{qFG_KMGMD2VB!enb}9DnV^xpy@H@ zU*Fo(5Kc(szMbjC_TC4G;N8#=Kb$wtNDv9sb@h)2)lwZE_Ymmg%Qkr-dP7=as_21H zjdXhUF0dc>|F@A@e)ZiR(7>tGRrk`|_!;>9WL)t#DD@ykdM2`k5Q2cy^Z<7?`6<H}NX_w(&^%v#~v9WRC}i$Ey!G>VR(yb(XD;j zuYJ~NhTENX4VNAqe$2DM`ZLz3s0xRe>Zci16?@GNrGKk`dASj@(B+L(zPudS)nHEv zsIQyrksSBARD86GXM^+4Ud-|j??`42Iwb+{$cFVt?C{1~CWonXTN}G=>OcDId;vzd z>(rCLEIsO+gE6s4e=D2I#0)>oAAPsH>>;E8WOVxFWr%)jBvDd}ecPSLa*~tzPFTUf ze(U)fHC?-);{{MekGu{9PCaWL%l-ZKLY5lpQ+hf+n`u?-c6PYPf-}%k&wj=@ZzYqO z3iILB)z#e`0S>9#hGKRV>45nRA0s5s3J#0{uUTX)%vz?WI^ZYxhozj&hzMO$S!UHW z;qIJ)OJoy?11C5h+O&Gf9uS}Q>3DgxZmw{Dif^+3KK_vmYaZw<&Dh1`1zG38@>;@| z_v_llg|yVgV^My{2g%2RnJt`FPRr0@*MuZe84Y$3gs|we2#o4l79`H#E70F9?(OJ6 zD^K(92voaRY}n=>GPd(gtxs*l;T?^l&8PG(XRuE=NRZE?0}4u)EC?(ln-Iim6Rqcv z;1o|vheArteo(jrD|YR3u)&AKm^dr(`Y5y4`)=js=+?i+C@+^~vItYb?c8o29JkE; zy56@m^hB)zq?b)162=_jU4w>UI>{+m?>ESrv-TyP>j}+p99|_r5&f$$zIn=}`ep)w zfRZs7jGAlnPXKejj?UV0KH`#|7U;4551nq3HA%%J3T`k`ayFPPXSS$Pr_^b773S^V zQqVC~O@3|$vvvSodeW5LGS?*kKu@&qG8^YQ)F<2c?ttYJmwJ2>pxWVklHWIY2(c;| z1fm^tIs^RTo7)G2mS27Hl}J<;II4Ek#PVveN1!Jv^&d7K9D|-f4z53N- zHu47P(1%@mcd1l*7E)>JRVZ?ah~aL{15B|;u~xpHZ`+Sf0w2m9a;jd~y&A(l*IWwC zCM2kI>|efB1TkG4GQ0S?e#2@|L#q%z&vyeTcM=Cb(S*Y#s`t*mU3A+Km|ZZ`S}0K{1FC7yNV0HRyy>sn+8;?;{G?OdaP zFauq)?jZ&pmLmE}W&6mD6sqTTjw>^;rGT7Z&X5@_h&4i}HS6$7=2xFw!z{ z#>*m+`;i+-&&>fEqn`j+TPdY;Q@tGw+|P1E6C;xXL7le^{!~CfKuWscIjV-Q=(9tk z(N&)L=K&&JZtYz>*4Gsu5`n14t<^quM$IGun;=KkFWDJjKp)OkI?GnCK_R=-f75Jx zdoFCDh=VE_?=Zf~s6Mbhyx}wt;-#*=Rv>bA$v9zou8AqSR}OSR?;Oofj!Ewys@NP7 zJhocPJ^k|o%c`1d*JF6Z_4t`1@o+E;mZ}+}NzdH)fzxt@AZ?3sZG7|C6um=_qUqOg zK{uoLBopVaigaPf;7FaW@N`NDU)w-;sM`J~8~@{EiQNnG=Rm>u3^C8=Z;s6M^e8-V z$k9i(IeDv-?grgtXJh6JDej`5F)+ps7bXEqnptl>nXh_2?nT2x6N+q8QqTbe)d$a) z7M>f)r3njt*CM-eFSc z*IFr!uAj7L76fIbBW5OZ+PHfaL{1TVUU}^+4p^a6=e4}Qi%l|HQP9;dC?sgE!8b{V zjOUTPm!~(Tgkesp-#78(L`$K1o1{OCnlDx8Q>D<-lz5=9-;uA6)Q@$I9IHJa5D?Qc zwUw~_OKfC6CJ_dMX_aN~fTuIP{kT3$m+Z6(Frw}=Rzw2e)YDvU*4>&o>0ebbg0#KZstj2T#r+qwR&;Ig&Rs-Vays zxQvW_JRv#z!6DaDPjIW)LLIuf(4D7Xd@gz73fN#3S;u_FU<`aL*NX3L-I@BW;DA3M zc`y`R-l|1xy}M>Qq-yBO`BKqO!xnzPLiT*hNKNjCeJOB9hjG9+NifXlivnXzfMyG! zEwwwml;q@5-QLa3U(FniZ(;$r-ZSa4*`qgJ?a5hr?+qvK9-Th-4L8*paK|ISe31k0 zS1y#k=zeV1Gz06qdKtjz2k_&z#ue2ZP64P1Z6jLp(7U?#!$nwYDCsmlX1J0?-IQ^n zRYyHN0Bvtje*|=6!Y#0k<%~cE?fnD<+I!3R9|0Mob0b_ahr`<_le%JmC!f?OXh!x=u+ZqliQ$sXkk8 zy5cRE-i0nO$OesLB`I319Dp)(;tL|bRA}+KoaKhmJme>(`2tktRlXM>?;Q;CD8Cku zUHrY_U4f|u8=|-ol}duG^=)>Z`thBnY^9UBLxC)>G;RnAgym#1+5MdKc`uoqjZ4Gf zaGiR&Gh}Tx9poiwS{WfP$xh&jRx-^oOks(~MRH|F_h>T~33Q_#Crc2TP4x2TgE8g^R3#f42$U8^>z>@SqBLL3Y4 zK^e#EZ=lq(UHZGNl2aXGQ(oxiYnSwGn<(4^AGYD?j*Nn{_dXWNyn{q(CZ-JnF^GI= zP_AuOCZbz7ZAr@q%a7{bQjcc+uxK)oyep@0+7f4MP>?G^YOW-Nxp0wbbwIv9Fe1q6 z-1Cf>a-X6ghi)VR{E2AR?bff=MRfPm3HYa4#rvgVs4Eyu=$8nN__zi&-Bi#x<8OpR z$5AsKa~+v`K(ok(Lk}?4#})v zTUP-!^ATuw>4e?M$bsIOCAoRgn!dDbndmUgyx;y)8RCp>Fej=BD3%G0o#< zkR_I95{mx)Dc4;#?}`2~Tv{LfLw%;h%IsR)+m*-VI?)}rn(kCTyS+-M11%T<1pwbtmz9!D*f;5hLw6O$Dmi!6yjMknmA6+kUPEZz?M;pfDY)BLc z>3D>Iy6#lh@xKK*w`38m+iP#q7J34Rm;MCUXSKKFu%=)iFFY{Fd(O6;I*#?V27#j3 z1D^I4oIp#f6GvuhzOxX}1*Q&g7)xndmZ5K`<|n1Q!_pMeUgwWe6c1tC?%_p7V}HX4 kY6Sw}|4*CJOM}IB;BQg9<1~P~fIvPM1HbRQaOKXw09HNL&Hw-a literal 23299 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 sz diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index ab8dee66..6f496771 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -178,6 +178,8 @@ "ssh_command_widget_error_message_unsupported_private_key": "Unsupported or invalid private key.", "ssh_key_error_passphrase_needed": "The private key is protected by a passphrase: enter it in the password field.", "ssh_key_error_passphrase_wrong": "The passphrase for the private key is wrong.", + "ssh_key_error_putty_key": "This is a PuTTY key (.ppk), which cannot be used here: load it in PuTTYgen, " + "choose Conversions > Export OpenSSH key, and pick the exported file.", "ssh_command_widget_error_message_key_auth_failed": "Key auth failed", "ssh_command_widget_status_label_connected": "Connected", "ssh_command_widget_status_label_disconnected": "Disconnected", @@ -273,7 +275,7 @@ "ssh_login_widget_placeholder_host": "Host (e.g., 192.168.0.10)", "ssh_login_widget_placeholder_username": "Username", "ssh_login_widget_placeholder_password": "Password", - "ssh_login_widget_placeholder_private_key": "Private key path (.pem/.ppk)", + "ssh_login_widget_placeholder_private_key": "Private key path (OpenSSH or PEM)", "ssh_login_widget_button_use_key_auth": "Use key auth", "ssh_login_widget_button_connect": "Connect", "ssh_login_widget_button_disconnect": "Disconnect", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index e8ea11f8..26c39c9e 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -171,6 +171,8 @@ "ssh_command_widget_error_message_unsupported_private_key": "不支援或無效的私鑰。", "ssh_key_error_passphrase_needed": "這把私鑰有密語保護:請在密碼欄輸入密語。", "ssh_key_error_passphrase_wrong": "私鑰的密語不對。", + "ssh_key_error_putty_key": "這是 PuTTY 金鑰(.ppk),這裡無法使用:請在 PuTTYgen 載入它," + "選 Conversions > Export OpenSSH key 匯出,再選擇匯出的檔案。", "ssh_command_widget_error_message_key_auth_failed": "金鑰驗證失敗", "ssh_command_widget_status_label_connected": "已連線", "ssh_command_widget_status_label_disconnected": "已斷線", @@ -263,7 +265,7 @@ "ssh_login_widget_placeholder_host": "主機(例如:192.168.0.10)", "ssh_login_widget_placeholder_username": "使用者名稱", "ssh_login_widget_placeholder_password": "密碼", - "ssh_login_widget_placeholder_private_key": "私鑰路徑(.pem/.ppk)", + "ssh_login_widget_placeholder_private_key": "私鑰路徑(OpenSSH 或 PEM)", "ssh_login_widget_button_use_key_auth": "使用金鑰驗證", "ssh_login_widget_button_connect": "連線", "ssh_login_widget_button_disconnect": "斷線", diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py index 7b068cc1..aea319f9 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py @@ -27,6 +27,10 @@ UNSUPPORTED_KEY = "ssh_command_widget_error_message_unsupported_private_key" PASSPHRASE_NEEDED = "ssh_key_error_passphrase_needed" PASSPHRASE_WRONG = "ssh_key_error_passphrase_wrong" +PUTTY_KEY = "ssh_key_error_putty_key" + +# A PuTTY key file (.ppk), which neither paramiko nor cryptography reads +_PUTTY_HEADER = b"PuTTY-User-Key-File-" # PKCS#8, which paramiko does not read (openssl genpkey, ssh-keygen -m PKCS8) _PKCS8_PLAIN = b"-----BEGIN PRIVATE KEY-----" @@ -50,13 +54,13 @@ def load_private_key(key_path: str, password: str, *, context: str = "SSH") -> p return _load_pkcs8(key_path, passphrase, context) -def _pkcs8_data(key_path: str) -> bytes | None: - """The contents of *key_path* if it is a PKCS#8 key file, else ``None``.""" +def _key_file_data(key_path: str, *headers: bytes) -> bytes | None: + """The contents of *key_path* if it starts with one of *headers*, else ``None``.""" try: data = Path(key_path).read_bytes().lstrip() except OSError: return None - return data if data.startswith((_PKCS8_PLAIN, _PKCS8_ENCRYPTED)) else None + return data if data.startswith(headers) else None def _load_pkcs8(key_path: str, passphrase: str | None, context: str) -> paramiko.PKey | None: @@ -66,7 +70,7 @@ def _load_pkcs8(key_path: str, passphrase: str | None, context: str) -> paramiko only, for paramiko to load. A passphrase is used only for an encrypted file: paramiko ignores one given for a plain key, and so does this. """ - data = _pkcs8_data(key_path) + data = _key_file_data(key_path, _PKCS8_PLAIN, _PKCS8_ENCRYPTED) if data is None: return None password = passphrase.encode("utf-8") if passphrase and data.startswith(_PKCS8_ENCRYPTED) else None @@ -96,10 +100,12 @@ def unloadable_key_reason(key_path: str, password: str) -> str: wrong only when it does not decrypt the file: an encrypted key of a type paramiko cannot load (DSA, a FIDO key) asks for one too, and with the right one it is still unsupported. An encrypted PKCS#8 file says so in - its first line, which paramiko does not read. + its first line, which paramiko does not read. A PuTTY key is to be + exported as an OpenSSH one. """ - pkcs8 = _pkcs8_data(key_path) - if pkcs8 is not None and pkcs8.startswith(_PKCS8_ENCRYPTED): + if _key_file_data(key_path, _PUTTY_HEADER) is not None: + return PUTTY_KEY + if _key_file_data(key_path, _PKCS8_ENCRYPTED) is not None: if not password: return PASSPHRASE_NEEDED return UNSUPPORTED_KEY if _decrypts(key_path, password) else PASSPHRASE_WRONG diff --git a/test/test_utils/test_ssh_security.py b/test/test_utils/test_ssh_security.py index 75590d0c..dd54d628 100644 --- a/test/test_utils/test_ssh_security.py +++ b/test/test_utils/test_ssh_security.py @@ -125,7 +125,8 @@ def test_each_reason_has_a_message(self): ) from pybreeze.pybreeze_ui.connect_gui.ssh import ssh_key_loader - for key in (ssh_key_loader.UNSUPPORTED_KEY, ssh_key_loader.PASSPHRASE_NEEDED, ssh_key_loader.PASSPHRASE_WRONG): + for key in (ssh_key_loader.UNSUPPORTED_KEY, ssh_key_loader.PASSPHRASE_NEEDED, ssh_key_loader.PASSPHRASE_WRONG, + ssh_key_loader.PUTTY_KEY): assert english_word_dict.get(key) and traditional_chinese_word_dict.get(key) @@ -189,6 +190,32 @@ def test_a_type_paramiko_cannot_use_is_still_unsupported(self, tmp_path, passphr assert unloadable_key_reason(path, typed) == UNSUPPORTED_KEY +class TestPuttyKeys: + """paramiko reads no PuTTY key; the login form offered .ppk and then called it invalid.""" + + _PPK = ("PuTTY-User-Key-File-3: ssh-ed25519\nEncryption: none\nComment: laptop\n" + "Public-Lines: 1\nAAAAC3NzaC1lZDI1NTE5AAAAIA==\nPrivate-Lines: 1\nAAAAIA==\n" + "Private-MAC: 00\n") + + @pytest.mark.parametrize("typed", ["", "a passphrase"]) + def test_the_user_is_told_to_export_it_as_openssh(self, tmp_path, typed): + from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import PUTTY_KEY, unloadable_key_reason + + path = tmp_path / "laptop.ppk" + path.write_text(self._PPK, encoding="ascii") + assert load_private_key(str(path), typed) is None + assert unloadable_key_reason(str(path), typed) == PUTTY_KEY + + def test_the_login_form_does_not_offer_it(self): + from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + + for words in (pybreeze_english_word_dict, pybreeze_traditional_chinese_word_dict): + assert ".ppk" not in words["ssh_login_widget_placeholder_private_key"] + + class TestSha1Algorithms: def test_a_transport_given_them_offers_no_sha1(self): import socket From 1ff35575ca2bb30cba95bcede1ef8ebeebd68905 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:19:33 +0800 Subject: [PATCH 060/166] Drop the private address from the SSH host placeholder --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/ssh_client.png | Bin 26098 -> 25067 bytes .../extend_multi_language/extend_english.py | 2 +- .../extend_traditional_chinese.py | 2 +- test/test_utils/test_language_parity.py | 8 ++++++++ 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index fd4180df..a5d29de1 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2444 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2445 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index da18ce6e..3d21fe4c 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -76,3 +76,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the SSH Client paragraph of the three READMEs says a `.ppk` key is exported as OpenSSH first; `images/ssh_client.png` redone at its 1180×640 under dark_amber with the new placeholder; `architecture_explore.md` (`ssh_key_loader.py`, test count). - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ssh_security.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `images/ssh_client.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-09 · 2026-09-25 · No private address in the SSH host placeholder · #fix #ssh #i18n + +- **What**: the SSH login form's host field suggested `Host (e.g., 192.168.0.10)` (主機(例如:192.168.0.10)), a hardcoded private address, against the project's rule of no hardcoded IPs or hostnames outside documented loopback; it was the only one in the package besides the CGNAT range `url_validation.py` checks against. +- **Fix**: the placeholder reads "Host name or IP address" (主機名稱或 IP 位址). +- **Tests**: `test_no_string_holds_an_ip_address` in `test_language_parity.py` (`TestLabelsAreTold`): no English or Traditional Chinese string holds a dotted IPv4 address other than loopback (the AI panels' placeholders name `127.0.0.1`). It failed on the two placeholders before the change. The language parity and SSH login form tests pass (23). +- **Result / numbers**: 2445 tests in 135 files. `ruff check` clean. +- **Docs**: `images/ssh_client.png` redone at 1180×640 under dark_amber with the new placeholder; `architecture_explore.md` (test count). +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `images/ssh_client.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 3dc039e3..8420ff86 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-09 | 2026-09-25 | No private address in the SSH host placeholder | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-08 | 2026-09-25 | Say what to do with a PuTTY key instead of offering it | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-07 | 2026-09-25 | SSH and SFTP log in with a PKCS#8 private key | #feature #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-06 | 2026-09-25 | The Mermaid paste box in the fixed-pitch font | #fix #diagram #ui | [2026-09-c](2026-09-c.md) | @@ -411,4 +412,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 8 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 9 | diff --git a/images/ssh_client.png b/images/ssh_client.png index 96441dc2ad61331e51d9fb4a57ba814564eb3df4..09814c60cbd6b1af3aa9045a17c09f4e5655fe98 100644 GIT binary patch literal 25067 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⁢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 I$t(Z(7vH$k?f?J) literal 26098 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 diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 6f496771..c576b46e 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -272,7 +272,7 @@ "ssh_login_widget_label_user": "User", "ssh_login_widget_label_key": "Key", "ssh_login_widget_label_password": "Password", - "ssh_login_widget_placeholder_host": "Host (e.g., 192.168.0.10)", + "ssh_login_widget_placeholder_host": "Host name or IP address", "ssh_login_widget_placeholder_username": "Username", "ssh_login_widget_placeholder_password": "Password", "ssh_login_widget_placeholder_private_key": "Private key path (OpenSSH or PEM)", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 26c39c9e..7ace556e 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -262,7 +262,7 @@ "ssh_login_widget_label_user": "使用者", "ssh_login_widget_label_key": "金鑰", "ssh_login_widget_label_password": "密碼", - "ssh_login_widget_placeholder_host": "主機(例如:192.168.0.10)", + "ssh_login_widget_placeholder_host": "主機名稱或 IP 位址", "ssh_login_widget_placeholder_username": "使用者名稱", "ssh_login_widget_placeholder_password": "密碼", "ssh_login_widget_placeholder_private_key": "私鑰路徑(OpenSSH 或 PEM)", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 4b910733..1a1a0ee9 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -109,6 +109,14 @@ def test_a_dock_is_titled_like_its_tab(self): assert EN[dock] == EN[tab] assert ZH[dock] == ZH[tab] + def test_no_string_holds_an_ip_address(self): + # The SSH host placeholder's example was a private address (CLAUDE.md: no + # hardcoded IPs or hostnames outside documented loopback); the AI panels' + # placeholders name 127.0.0.1 + addresses = {key: value for words in (EN, ZH) for key, value in words.items() + if re.search(r"\b(?!127\.)\d{1,3}(\.\d{1,3}){3}\b", str(value))} + assert addresses == {} + def test_no_label_is_in_capitals(self): # Every automation menu's Help submenu read "HELP", beside JEditor's "Help" menu acronyms = {"HTTP", "JSON", "MIME", "SFTP"} From 9bc5450352dd50c5d3997920ed22df2e8531cc02 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:26:35 +0800 Subject: [PATCH 061/166] Show the JWT, Query and URL tools' boxes in the fixed-pitch font --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/tools_montage_a.png | Bin 123059 -> 122264 bytes images/tools_montage_b.png | Bin 79899 -> 78625 bytes .../pybreeze_ui/tools_gui/jwt_decoder_gui.py | 3 +++ .../pybreeze_ui/tools_gui/query_json_gui.py | 3 +++ .../pybreeze_ui/tools_gui/url_builder_gui.py | 3 +++ .../test_utils/test_code_boxes_fixed_pitch.py | 18 ++++++++++++++++++ 9 files changed, 40 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index a5d29de1..88657c2c 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2445 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2451 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 3d21fe4c..bebf1d8c 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -86,3 +86,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: `images/ssh_client.png` redone at 1180×640 under dark_amber with the new placeholder; `architecture_explore.md` (test count). - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `images/ssh_client.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-10 · 2026-09-25 · JWT, Query and URL tools in the fixed-pitch font · #fix #tools #ui + +- **What**: U-20260924-314 put the code boxes of the tool tabs in the fixed-pitch font but left out three tools whose boxes hold the same kind of text: the JWT decoder (a token in, its header and payload as indented JSON out), Query ↔ JSON and the URL parser / builder (a query string or URL in, JSON out, and back). Their JSON lost its indentation's alignment in the proportional font. +- **Change**: `JwtDecoderGUI`, `QueryJsonGUI` and `UrlBuilderGUI` call `use_fixed_pitch_font()` on their input and output boxes. The tools that show prose or a list (HTTP status, header and response analysis, hashes, timestamps, the regex matches) keep the interface font, as before. +- **Tests**: `test_code_boxes_fixed_pitch.py` covers the six boxes; all failed before the change. With the three tools' widget tests, 113 pass. +- **Result / numbers**: 2451 tests in 135 files. `ruff check` clean. +- **Docs**: the JWT panel of `images/tools_montage_a.png` and the Query and URL panels of `images/tools_montage_b.png` redone under dark_amber at their sizes with the same content (the JWT rebuilt from the one shown, byte for byte). +- **Files**: `pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py`, `pybreeze/pybreeze_ui/tools_gui/query_json_gui.py`, `pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, `images/tools_montage_a.png`, `images/tools_montage_b.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8420ff86..95839840 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-10 | 2026-09-25 | JWT, Query and URL tools in the fixed-pitch font | #fix #tools #ui | [2026-09-c](2026-09-c.md) | | U-20260925-09 | 2026-09-25 | No private address in the SSH host placeholder | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-08 | 2026-09-25 | Say what to do with a PuTTY key instead of offering it | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-07 | 2026-09-25 | SSH and SFTP log in with a PKCS#8 private key | #feature #ssh | [2026-09-c](2026-09-c.md) | @@ -412,4 +413,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 9 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 10 | diff --git a/images/tools_montage_a.png b/images/tools_montage_a.png index 5f919c41388b953bbd75e04cf70653211c28c890..5e555fa04a93c649fed466376b0891491e9a3dd8 100644 GIT binary patch literal 122264 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%0A2kn19srod@BQ2P@YitC@|CJ=jY*fdy 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 z%0A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 HT0Z<=se9?v diff --git a/images/tools_montage_b.png b/images/tools_montage_b.png index 440d7ee6365496890ac9aa62a10ee5551b621820..fe0580a6f3913c2f60e4684c7aba60064184f6f6 100644 GIT binary patch delta 43412 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 f^qc($8QW+Fb}M str: @@ -57,6 +58,7 @@ def __init__(self, initial_token: str | None = None, main_window=None) -> None: self.input_edit = QTextEdit() self.input_edit.setPlaceholderText(word.get("jwt_decoder_input_placeholder")) self.input_edit.setAcceptRichText(False) + use_fixed_pitch_font(self.input_edit) self.decode_button = QPushButton(word.get("jwt_decoder_decode_button")) self.decode_button.clicked.connect(self.decode) @@ -64,6 +66,7 @@ def __init__(self, initial_token: str | None = None, main_window=None) -> None: self.output_label = QLabel(word.get("jwt_decoder_output_label")) self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) + use_fixed_pitch_font(self.output_edit) self.actions = OutputActions( self, self.output_edit, main_window=main_window, diff --git a/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py b/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py index 9045374f..717770b7 100644 --- a/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py @@ -12,6 +12,7 @@ from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.utils.query_tools.query_convert import json_to_query, query_to_json from pybreeze.pybreeze_ui.error_text import error_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font class QueryJsonGUI(QWidget): @@ -29,6 +30,7 @@ def __init__(self, main_window=None) -> None: self.input_edit = QTextEdit() self.input_edit.setPlaceholderText(word.get("query_json_input_placeholder")) self.input_edit.setAcceptRichText(False) + use_fixed_pitch_font(self.input_edit) self.to_json_button = QPushButton(word.get("query_json_to_json_button")) self.to_json_button.clicked.connect(self.convert_to_json) @@ -42,6 +44,7 @@ def __init__(self, main_window=None) -> None: self.output_label = QLabel(word.get("query_json_output_label")) self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) + use_fixed_pitch_font(self.output_edit) self.actions = OutputActions( self, self.output_edit, main_window=main_window, diff --git a/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py b/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py index 017bd782..a1ccd76c 100644 --- a/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py @@ -12,6 +12,7 @@ from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.utils.url_tools.url_convert import json_to_url, url_to_json from pybreeze.pybreeze_ui.error_text import error_text +from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font class UrlBuilderGUI(QWidget): @@ -30,6 +31,7 @@ def __init__(self, main_window=None, initial_url: str | None = None) -> None: self.input_edit = QTextEdit() self.input_edit.setPlaceholderText(word.get("url_builder_input_placeholder")) self.input_edit.setAcceptRichText(False) + use_fixed_pitch_font(self.input_edit) self.to_json_button = QPushButton(word.get("url_builder_to_json_button")) self.to_json_button.clicked.connect(self.convert_to_json) @@ -43,6 +45,7 @@ def __init__(self, main_window=None, initial_url: str | None = None) -> None: self.output_label = QLabel(word.get("url_builder_output_label")) self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) + use_fixed_pitch_font(self.output_edit) self.actions = OutputActions( self, self.output_edit, main_window=main_window, diff --git a/test/test_utils/test_code_boxes_fixed_pitch.py b/test/test_utils/test_code_boxes_fixed_pitch.py index b4a94753..3052a337 100644 --- a/test/test_utils/test_code_boxes_fixed_pitch.py +++ b/test/test_utils/test_code_boxes_fixed_pitch.py @@ -53,6 +53,21 @@ def _cot_code_review(): return CoTCodeReviewGUI() +def _jwt_decoder(): + from pybreeze.pybreeze_ui.tools_gui.jwt_decoder_gui import JwtDecoderGUI + return JwtDecoderGUI() + + +def _query_json(): + from pybreeze.pybreeze_ui.tools_gui.query_json_gui import QueryJsonGUI + return QueryJsonGUI() + + +def _url_builder(): + from pybreeze.pybreeze_ui.tools_gui.url_builder_gui import UrlBuilderGUI + return UrlBuilderGUI() + + def _mermaid_import(): from pybreeze.pybreeze_ui.diagram_editor.diagram_editor_widget import MermaidImportDialog return MermaidImportDialog() @@ -63,6 +78,9 @@ def _mermaid_import(): (_curl, "input_edit"), (_curl, "output_edit"), (_har, "output_edit"), (_json_format, "input_edit"), (_json_format, "output_edit"), + (_jwt_decoder, "input_edit"), (_jwt_decoder, "output_edit"), + (_query_json, "input_edit"), (_query_json, "output_edit"), + (_url_builder, "input_edit"), (_url_builder, "output_edit"), (_ai_code_review, "code_input"), (_cot_code_review, "code_paste_area"), (_mermaid_import, "_editor"), From cfe84f6e2e360bd044e2e6b35e441a908b26f769 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:30:26 +0800 Subject: [PATCH 062/166] Stop the AI panels suggesting an endpoint they refuse --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 10 ++++++ docs/updates/README.md | 3 +- images/ai_code_review.png | Bin 31048 -> 31172 bytes images/skills_send.png | Bin 34381 -> 34381 bytes progress.md | 1 + .../extend_multi_language/extend_english.py | 4 +-- .../extend_traditional_chinese.py | 4 +-- test/test_utils/test_language_parity.py | 29 ++++++++++++++++-- 12 files changed, 48 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b7cb1ede..79b7314e 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ Password or private-key authentication (the key file picked with Browse, startin ![AI code review client](images/ai_code_review.png) -*Shown in the pre-send state.* Send a selection to an LLM endpoint, then accept or reject the suggestion — the tally is kept in `~/.pybreeze/response_stats.txt`. The URL is SSRF-validated, the connection goes only to the address that was checked, redirects are not followed, and the response body is size-capped before it reaches the panel. +*Shown in the pre-send state.* Send a selection to an LLM endpoint, then accept or reject the suggestion — the tally is kept in `~/.pybreeze/response_stats.txt`. The URL is SSRF-validated, the connection goes only to the address that was checked, redirects are not followed, and the response body is size-capped before it reaches the panel. An endpoint on this machine or on a private network, such as a local model server, is therefore refused; CoT Code Review and Skill Send check their endpoint URL the same way. ### Chain-of-Thought Code Review (prthinker) diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index afddc08e..a8907176 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -165,7 +165,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![AI 代码审查客户端](../images/ai_code_review.png) -*图中为发送前的状态。* 把选中的代码发送到 LLM 端点,然后接受或拒绝建议——统计记录保存在 `~/.pybreeze/response_stats.txt`。URL 会经过 SSRF 验证,连接只会连到经过检查的地址,不跟随重定向,响应体在到达面板之前会被限制大小。 +*图中为发送前的状态。* 把选中的代码发送到 LLM 端点,然后接受或拒绝建议——统计记录保存在 `~/.pybreeze/response_stats.txt`。URL 会经过 SSRF 验证,连接只会连到经过检查的地址,不跟随重定向,响应体在到达面板之前会被限制大小。因此本机或私有网络上的端点(例如在本机运行的模型服务器)会被拒绝;CoT Code Review 与 Skill Send 也以同样的方式检查端点 URL。 ### 思维链代码审查(prthinker) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 956b9237..e5837fc4 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -165,7 +165,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![AI 程式碼審查用戶端](../images/ai_code_review.png) -*畫面為送出前的狀態。* 把選取的程式碼送到 LLM 端點,再接受或拒絕建議——統計會記錄在 `~/.pybreeze/response_stats.txt`。URL 會經過 SSRF 驗證,連線只會連到檢查過的位址,不跟隨重新導向,回應本文在送進面板前也有大小上限。 +*畫面為送出前的狀態。* 把選取的程式碼送到 LLM 端點,再接受或拒絕建議——統計會記錄在 `~/.pybreeze/response_stats.txt`。URL 會經過 SSRF 驗證,連線只會連到檢查過的位址,不跟隨重新導向,回應本文在送進面板前也有大小上限。因此本機或私有網路上的端點(例如在本機跑的模型伺服器)會被拒絕;CoT Code Review 與 Skill Send 也用同樣的方式檢查端點 URL。 ### 思維鏈程式碼審查(prthinker) diff --git a/architecture_explore.md b/architecture_explore.md index 88657c2c..d705b554 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2451 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2452 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index bebf1d8c..95d6b81b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -96,3 +96,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the JWT panel of `images/tools_montage_a.png` and the Query and URL panels of `images/tools_montage_b.png` redone under dark_amber at their sizes with the same content (the JWT rebuilt from the one shown, byte for byte). - **Files**: `pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py`, `pybreeze/pybreeze_ui/tools_gui/query_json_gui.py`, `pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py`, `test/test_utils/test_code_boxes_fixed_pitch.py`, `images/tools_montage_a.png`, `images/tools_montage_b.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-11 · 2026-09-25 · AI panels no longer suggest an endpoint they refuse · #fix #ai #readme + +- **What**: the endpoint field of CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` (請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api), and the README's AI Code Review and Skill Send screenshots showed `http://127.0.0.1:8000/...` typed in; but every AI panel sends through `validate_url`, which refuses loopback and private addresses ("Access to non-public address 127.0.0.1 is blocked."). Following the example could only fail. The README did not say that a local endpoint is refused. +- **Fix**: both placeholders read "The API URL to send to, e.g. https://llm.example.com/api" (要傳送到的 API URL,例如 https://llm.example.com/api). The AI Code Review paragraph of the three READMEs says an endpoint on this machine or a private network, such as a local model server, is refused, and that CoT Code Review and Skill Send check theirs the same way. Whether they should let the user allow one is the owner's decision: progress #111. +- **Tests**: `test_an_example_url_is_one_the_ide_would_send_to` in `test_language_parity.py` (`TestLabelsAreTold`) runs every URL in either language's strings through `validate_url`, names resolved without the network (an address literal as itself, `localhost` as loopback, any other name as a public address). It failed on the two placeholders before the change; the cURL import's `https://api.example.com/...` passes. With the CoT session and keys-used tests, 28 pass. +- **Result / numbers**: 2452 tests in 135 files. `ruff check` clean. +- **Docs**: `images/ai_code_review.png` and `images/skills_send.png` redone at their sizes under dark_amber, from the demo home, with `https://llm.example.com/...` typed in; `progress.md` #111 [DECIDE]; `architecture_explore.md` (test count). +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `images/ai_code_review.png`, `images/skills_send.png`, `progress.md`, `architecture_explore.md` +- **Open items**: `progress.md` #111. diff --git a/docs/updates/README.md b/docs/updates/README.md index 95839840..66574eb6 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-11 | 2026-09-25 | AI panels no longer suggest an endpoint they refuse | #fix #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-10 | 2026-09-25 | JWT, Query and URL tools in the fixed-pitch font | #fix #tools #ui | [2026-09-c](2026-09-c.md) | | U-20260925-09 | 2026-09-25 | No private address in the SSH host placeholder | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-08 | 2026-09-25 | Say what to do with a PuTTY key instead of offering it | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | @@ -413,4 +414,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 10 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 11 | diff --git a/images/ai_code_review.png b/images/ai_code_review.png index 38185e7724d0411b2ff61c2478ce76f78462d85b..e22c0cdfcb233fb67850402d75d82f5324bfb639 100644 GIT binary patch literal 31172 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 CI3*GQ diff --git a/images/skills_send.png b/images/skills_send.png index 5ca71509d54a7bdba647592dd55e8b491eafed22..c5389662510aeb20fb496bf96b7bb34c482b620e 100644 GIT binary patch literal 34381 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 Dd#*Yx literal 34381 zcmcG$2UJsA+b+u9wrxSIAfTYqq;Gl$l_p(UfY21A_g({v1*M48&^v@4DWMZo1f)YK zp(7wIA%v0;NPwIL``i2c|M<@T-*fJ{*D)9@&B|JHuKB*te4gihmmi*LtI}Rzx97ni5CscPWpBIHd}u4-+{Z+&@6SE z&Hi?BM03si>EK+y!+y22OTafH0 zz6DfGI4gIXw+6l)gInZ?l_ohZ*K}cgbaYZQIn_`#UDrqV8=WE&py>(u#8RUN^70j5 zcHp%Bx;hG5_3RG7_XBL;W>pUt6^^nWQlcg=9*^dwf_4`pfyc1laF{dtqAbIW8~Ion zOD@>(nvX*DL|K1EMeIyw0k5cd+YAfsOGC8d^PmJnJ>%x>};959@M71kT{!VP)2u_9I0@=kJ?) zKbJEs1-CEy+E$w|jL2HzCTqr$%1t%Ic0YZ(;)6q}#a_KSS>=*8U+duD5a{#s?&0A; zYXQ^AM1J-%c=PZ``K~8}$_^|f%;rq+kiFOvqte1jgy3KvHAo$@(bTypUs);SIMtZA zQBRS-BNwPy8fsRGZSqEK#^&wRGH#7MQ`-<(rR?I-&29|#2X6#5IF_fYI2`7Xhk<*Q zR0p&G-j3qMZ;Grkz?{TUx;d~s-cFwl;|sO*(O8)-tIDFOXtY!Rv|D0QQsFZ}yFnSt zplIKX$#R$0Ljko^(oC>VV_{xcn07GiQ&5I~yvF3}q=1CONOGm8C_H z;|~2|^~&)xq)Cs`kIPQ$)z^${h8+Z%;P zm>$6g;^Hd6vy(42{>UQ32^%i1A0PjHptG}mZF8PYxz@4)bMBCWzdRFQN{TOGIJpBG zjNmuOrE>@#kR>V{?oxbRsEa!ADKW?KhbsmZ`Mhvku1cTj(jloY$^;YGLP~4j|K?if z)ZUFJuC~#Y=(R^&xJYZbg00YPJ4-~kZ(WO_G?V64F++#8?HwIAOhd3qP$ml_y5pO@ z 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 diff --git a/progress.md b/progress.md index 27bfb270..82a0c849 100644 --- a/progress.md +++ b/progress.md @@ -19,3 +19,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. - **#109** [DECIDE] `je-editor>=1.0.27` (`requirements.txt`, `pyproject.toml`, `dev.toml`) lets an environment keep a gitpython below 3.1.59, which has CVE-2026-78676 (a config write turns a quoted value into a live `core.hooksPath`: code runs on the next hook) and CVE-2026-78679; the IDE's git features use it (JEditor's `je_editor/git_client/`). je-editor 1.0.28 differs from 1.0.27 only in requiring `gitpython>=3.1.59`, and 3.1.59 still has CVE-2026-87818 (the diff API's `--no-index` reads any path), fixed in 3.1.60. Should the dependencies say `je-editor>=1.0.28`, declare `gitpython>=3.1.60`, or both? JEditor's own floor is its to raise (`D:\Codes\JEDITOR\pyproject.toml:18`). - **#110** [BLOCKED] A file opened from the project tree shows as unsaved (" *" on its tab, `_is_modified` true) before anything is typed: JEditor's `EditorWidget.open_an_file` fills the editor with `setPlainText`, which fires `_on_text_changed`, and never clears the mark afterwards (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:295` and `:347-358` in `D:\Codes\JEDITOR` at 1bf81e3; plain `EditorMain` does the same). Needs `open_an_file` to clear the mark (`mark_saved()`) once the file is loaded. +- **#111** [DECIDE] AI Code Review, CoT Code Review and Skill Send refuse an endpoint on this machine or on a private network (`validate_url` in `ai_code_review_gui.py:77`, `code_review_thread.py:33`, `skills_send_gui.py:55`; `url_validation._is_blocked_ip`), so a local model server (Ollama, LM Studio, a prthinker server on localhost) cannot be used from them, although CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` until U-20260925-11. The Security › Network rule rejects loopback and private addresses for every user-supplied URL. Should these panels let the user allow a loopback (or private) endpoint, for example after a confirmation naming the address, or stay public-only? diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index c576b46e..b53d459b 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -359,7 +359,7 @@ # CoT code-review GUI "cot_gui_window_title": "Prompt Sender UI", "cot_gui_label_api_url": "API URL:", - "cot_gui_placeholder_api_url": "Please enter the API URL to send, e.g. http://127.0.0.1:5000/api", + "cot_gui_placeholder_api_url": "The API URL to send to, e.g. https://llm.example.com/api", "cot_gui_placeholder_code_paste_area": "Paste the code to review here", "cot_gui_label_step": "Step:", "cot_gui_placeholder_no_answers": "No answers yet", @@ -373,7 +373,7 @@ "skills_error_status": "Error: {status_code}\n{text}", "skills_exception": "Exception occurred: {error}", "skills_api_url_label": "LLM API URL:", - "skills_api_url_placeholder": "Enter the API URL to send, e.g. http://127.0.0.1:5000/api", + "skills_api_url_placeholder": "The API URL to send to, e.g. https://llm.example.com/api", "skills_prompt_select_label": "Select Prompt Template:", "skills_prompt_label": "Prompt:", "skills_send_button": "Send", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 7ace556e..0d963b8b 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -349,7 +349,7 @@ # CoT code-review GUI "cot_gui_window_title": "Prompt 傳送介面", "cot_gui_label_api_url": "API URL:", - "cot_gui_placeholder_api_url": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", + "cot_gui_placeholder_api_url": "要傳送到的 API URL,例如 https://llm.example.com/api", "cot_gui_placeholder_code_paste_area": "在這裡貼上要審查的程式碼", "cot_gui_label_step": "步驟:", "cot_gui_placeholder_no_answers": "還沒有回覆", @@ -363,7 +363,7 @@ "skills_error_status": "錯誤:{status_code}\n{text}", "skills_exception": "發生例外:{error}", "skills_api_url_label": "LLM API URL:", - "skills_api_url_placeholder": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", + "skills_api_url_placeholder": "要傳送到的 API URL,例如 https://llm.example.com/api", "skills_prompt_select_label": "選擇 Prompt 範本:", "skills_prompt_label": "Prompt:", "skills_send_button": "傳送", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 1a1a0ee9..fd692abb 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -111,12 +111,37 @@ def test_a_dock_is_titled_like_its_tab(self): def test_no_string_holds_an_ip_address(self): # The SSH host placeholder's example was a private address (CLAUDE.md: no - # hardcoded IPs or hostnames outside documented loopback); the AI panels' - # placeholders name 127.0.0.1 + # hardcoded IPs or hostnames outside documented loopback) addresses = {key: value for words in (EN, ZH) for key, value in words.items() if re.search(r"\b(?!127\.)\d{1,3}(\.\d{1,3}){3}\b", str(value))} assert addresses == {} + def test_an_example_url_is_one_the_ide_would_send_to(self, monkeypatch): + # CoT Code Review and Skill Send suggested http://127.0.0.1:5000/api, which + # the URL check they send through refuses as not public + import ipaddress + import socket + + from pybreeze.utils.network.url_validation import UnsafeURLError, validate_url + + def getaddrinfo(host, *_args, **_kwargs): + try: + address = str(ipaddress.ip_address(host)) + except ValueError: + address = "127.0.0.1" if host == "localhost" else "8.8.8.8" + return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (address, 0))] + + monkeypatch.setattr(socket, "getaddrinfo", getaddrinfo) + refused = {} + for words in (EN, ZH): + for key, value in words.items(): + for url in re.findall(r"https?://[^\s'\",))]+", str(value)): + try: + validate_url(url) + except UnsafeURLError as error: + refused[key] = (url, str(error)) + assert refused == {} + def test_no_label_is_in_capitals(self): # Every automation menu's Help submenu read "HELP", beside JEditor's "Help" menu acronyms = {"HTTP", "JSON", "MIME", "SFTP"} From 59d1fa1acbeaa851eb25f307ac74afbd2a83ffc2 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:32:13 +0800 Subject: [PATCH 063/166] Start AI Code Review on a method that sends the code --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/ai_code_review.png | Bin 31172 -> 31536 bytes .../connect_gui/url/ai_code_review_gui.py | 3 +++ test/test_utils/test_ai_code_review_client.py | 4 ++++ 9 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 79b7314e..c001928b 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ Password or private-key authentication (the key file picked with Browse, startin ![AI code review client](images/ai_code_review.png) -*Shown in the pre-send state.* Send a selection to an LLM endpoint, then accept or reject the suggestion — the tally is kept in `~/.pybreeze/response_stats.txt`. The URL is SSRF-validated, the connection goes only to the address that was checked, redirects are not followed, and the response body is size-capped before it reaches the panel. An endpoint on this machine or on a private network, such as a local model server, is therefore refused; CoT Code Review and Skill Send check their endpoint URL the same way. +*Shown in the pre-send state.* Send a selection to an LLM endpoint (as the form field `code` in the body of a POST, the default, or a PUT; GET and DELETE send the URL alone), then accept or reject the suggestion — the tally is kept in `~/.pybreeze/response_stats.txt`. The URL is SSRF-validated, the connection goes only to the address that was checked, redirects are not followed, and the response body is size-capped before it reaches the panel. An endpoint on this machine or on a private network, such as a local model server, is therefore refused; CoT Code Review and Skill Send check their endpoint URL the same way. ### Chain-of-Thought Code Review (prthinker) diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index a8907176..238df40c 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -165,7 +165,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![AI 代码审查客户端](../images/ai_code_review.png) -*图中为发送前的状态。* 把选中的代码发送到 LLM 端点,然后接受或拒绝建议——统计记录保存在 `~/.pybreeze/response_stats.txt`。URL 会经过 SSRF 验证,连接只会连到经过检查的地址,不跟随重定向,响应体在到达面板之前会被限制大小。因此本机或私有网络上的端点(例如在本机运行的模型服务器)会被拒绝;CoT Code Review 与 Skill Send 也以同样的方式检查端点 URL。 +*图中为发送前的状态。* 把选中的代码发送到 LLM 端点(以 POST(默认)或 PUT 发送,代码放在请求体的表单字段 `code` 中;GET 与 DELETE 只发送 URL),然后接受或拒绝建议——统计记录保存在 `~/.pybreeze/response_stats.txt`。URL 会经过 SSRF 验证,连接只会连到经过检查的地址,不跟随重定向,响应体在到达面板之前会被限制大小。因此本机或私有网络上的端点(例如在本机运行的模型服务器)会被拒绝;CoT Code Review 与 Skill Send 也以同样的方式检查端点 URL。 ### 思维链代码审查(prthinker) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index e5837fc4..99de4965 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -165,7 +165,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![AI 程式碼審查用戶端](../images/ai_code_review.png) -*畫面為送出前的狀態。* 把選取的程式碼送到 LLM 端點,再接受或拒絕建議——統計會記錄在 `~/.pybreeze/response_stats.txt`。URL 會經過 SSRF 驗證,連線只會連到檢查過的位址,不跟隨重新導向,回應本文在送進面板前也有大小上限。因此本機或私有網路上的端點(例如在本機跑的模型伺服器)會被拒絕;CoT Code Review 與 Skill Send 也用同樣的方式檢查端點 URL。 +*畫面為送出前的狀態。* 把選取的程式碼送到 LLM 端點(以 POST(預設)或 PUT 送出,程式碼放在本文的表單欄位 `code`;GET 與 DELETE 只送出 URL),再接受或拒絕建議——統計會記錄在 `~/.pybreeze/response_stats.txt`。URL 會經過 SSRF 驗證,連線只會連到檢查過的位址,不跟隨重新導向,回應本文在送進面板前也有大小上限。因此本機或私有網路上的端點(例如在本機跑的模型伺服器)會被拒絕;CoT Code Review 與 Skill Send 也用同樣的方式檢查端點 URL。 ### 思維鏈程式碼審查(prthinker) diff --git a/architecture_explore.md b/architecture_explore.md index d705b554..f258df87 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2452 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2453 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 95d6b81b..04c16929 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -106,3 +106,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: `images/ai_code_review.png` and `images/skills_send.png` redone at their sizes under dark_amber, from the demo home, with `https://llm.example.com/...` typed in; `progress.md` #111 [DECIDE]; `architecture_explore.md` (test count). - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `images/ai_code_review.png`, `images/skills_send.png`, `progress.md`, `architecture_explore.md` - **Open items**: `progress.md` #111. + +## U-20260925-12 · 2026-09-25 · AI Code Review sends the code by default · #fix #ai #readme + +- **What**: AI Code Review's Method box started on GET, and only POST and PUT carry the code (`METHODS_WITH_A_BODY`: the form field `code` in the body). Pasting code and pressing Send Request as the panel opened sent the URL alone; the endpoint never saw the code, and nothing said so. The README did not say how the code is sent. +- **Fix**: a new panel starts on POST (`DEFAULT_METHOD` in `connect_gui/url/ai_code_review_gui.py`); the list and its order are unchanged. The AI Code Review paragraph of the three READMEs says the code goes as the form field `code` in the body of a POST (the default) or a PUT, and that GET and DELETE send the URL alone. +- **Tests**: `test_a_new_panel_sends_the_code` in `test_ai_code_review_client.py`: a new panel's method is one that carries the code. It failed before the fix (`'GET' in ('POST', 'PUT')`). With the fixed-pitch tests, 48 pass. +- **Result / numbers**: 2453 tests in 135 files. `ruff check` clean. +- **Docs**: `images/ai_code_review.png` redone (Method: POST); the three READMEs; `architecture_explore.md` (test count). +- **Files**: `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `test/test_utils/test_ai_code_review_client.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `images/ai_code_review.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 66574eb6..6c172633 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-12 | 2026-09-25 | AI Code Review sends the code by default | #fix #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-11 | 2026-09-25 | AI panels no longer suggest an endpoint they refuse | #fix #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-10 | 2026-09-25 | JWT, Query and URL tools in the fixed-pitch font | #fix #tools #ui | [2026-09-c](2026-09-c.md) | | U-20260925-09 | 2026-09-25 | No private address in the SSH host placeholder | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | @@ -414,4 +415,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 11 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 12 | diff --git a/images/ai_code_review.png b/images/ai_code_review.png index e22c0cdfcb233fb67850402d75d82f5324bfb639..7f1c12c9d89f8d915c46f4f23bd9f2ca8ace6891 100644 GIT binary patch literal 31536 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 My9-r+zW($70y~m!y#N3J literal 31172 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 bool: SUPPORTED_METHODS = ("GET", "POST", "PUT", "DELETE") # The methods that carry the code in a body / 會把程式碼放進 body 的方法 METHODS_WITH_A_BODY = ("POST", "PUT") +# What a new panel sends with: GET carries no body, so the code went nowhere +DEFAULT_METHOD = "POST" # A saved count longer than this is not a count this panel wrote _MAX_COUNT_DIGITS = 16 @@ -188,6 +190,7 @@ def _build_request_row(self) -> QHBoxLayout: method_layout.addWidget(QLabel(self.word_dict.get("ai_code_review_gui_label_method"))) self.method_box = QComboBox() self.method_box.addItems(list(SUPPORTED_METHODS)) + self.method_box.setCurrentText(DEFAULT_METHOD) method_layout.addWidget(self.method_box) top_layout.addLayout(method_layout) return top_layout diff --git a/test/test_utils/test_ai_code_review_client.py b/test/test_utils/test_ai_code_review_client.py index a180c758..f64888e6 100644 --- a/test/test_utils/test_ai_code_review_client.py +++ b/test/test_utils/test_ai_code_review_client.py @@ -116,6 +116,10 @@ def request(sent_method, *_args, **_kwargs): assert sent == [method] # sent with the method chosen in the panel return answered, failed + def test_a_new_panel_sends_the_code(self, client): + # It started on GET, which sends no body: the code pasted went nowhere + assert client.method_box.currentText() in ai_code_review_gui.METHODS_WITH_A_BODY + def test_an_answer_reaches_the_panel(self, app, monkeypatch): class Response: ok = True From 237d60db65481df9db989f115402de875e5dd952 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:34:04 +0800 Subject: [PATCH 064/166] Document what CoT Code Review and Skill Send send --- README.md | 4 +- README/README_zh-CN.md | 4 +- README/README_zh-TW.md | 4 +- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++ docs/updates/README.md | 3 +- test/test_utils/test_ai_request_formats.py | 72 ++++++++++++++++++++++ 7 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 test/test_utils/test_ai_request_formats.py diff --git a/README.md b/README.md index c001928b..cb575131 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ One settings form holds the inference backend (`remote`, `local`, OpenAI-compati Create and manage the multi-step review chain: first summary → first code review → a judge of that review → linter → code smell detector → step-by-step analysis → total summary → a judge of the summary. Each step quotes the answers it needs from the steps before it. Files are watched, so an external edit shows up immediately. -Run the chain from **Tools → AI → CoT Code Review** (a tab, or a dock from the Dock menu): paste the code, give the endpoint URL, and each step's answer appears in the selector as it arrives. +Run the chain from **Tools → AI → CoT Code Review** (a tab, or a dock from the Dock menu): paste the code, give the endpoint URL, and each step's answer appears in the selector as it arrives. Each step is a POST of the JSON `{"prompt": "..."}`, and the response body, as text, is that step's answer. ### Skill Prompt Editor & Skill Send @@ -189,7 +189,7 @@ Run the chain from **Tools → AI → CoT Code Review** (a tab, or a dock from t |---|---| | ![Skill prompt editor](images/skill_prompt_editor.png) | ![Skill send](images/skills_send.png) | -Define reusable skill prompts (code explanation, code review), then pick one, edit it if needed, and send it to an LLM endpoint from a dedicated tab or dock. *Both shown in the pre-send state — no endpoint was contacted for these screenshots.* +Define reusable skill prompts (code explanation, code review), then pick one, edit it if needed, and send it to an LLM endpoint from a dedicated tab or dock: a POST of the JSON `{"code": "..."}` holding the prompt, whose response body is shown as it is. *Both shown in the pre-send state — no endpoint was contacted for these screenshots.* --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 238df40c..8703385b 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -181,7 +181,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: 创建和管理多步骤的审查链:第一次摘要 → 第一次代码审查 → 对该审查的评判 → linter → 代码坏味道检测 → 逐步分析 → 总摘要 → 对摘要的评判。每一步都会引用它所需的前面步骤的回答。文件受到监视,因此外部的编辑会立即显示出来。 -从 **Tools → AI → CoT Code Review**(一个标签页,或从 Dock 菜单打开停靠面板)运行这条审查链:粘贴代码,填入端点 URL,每一步的回答一到达就会出现在选择器中。 +从 **Tools → AI → CoT Code Review**(一个标签页,或从 Dock 菜单打开停靠面板)运行这条审查链:粘贴代码,填入端点 URL,每一步的回答一到达就会出现在选择器中。每一步都以 POST 发送 JSON `{"prompt": "..."}`,响应体(按文本读取)就是这一步的回答。 ### Skill 提示词编辑器与 Skill Send @@ -189,7 +189,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: |---|---| | ![Skill 提示词编辑器](../images/skill_prompt_editor.png) | ![Skill Send](../images/skills_send.png) | -定义可重复使用的技能提示词(代码解说、代码审查),然后选择一个,按需编辑,再从专用的标签页或停靠面板发送到 LLM 端点。*两张图都是发送前的状态——拍摄这些截图时没有连接任何端点。* +定义可重复使用的技能提示词(代码解说、代码审查),然后选择一个,按需编辑,再从专用的标签页或停靠面板发送到 LLM 端点:以 POST 发送装有提示词的 JSON `{"code": "..."}`,响应体按原样显示。*两张图都是发送前的状态——拍摄这些截图时没有连接任何端点。* --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 99de4965..8852c9ce 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -181,7 +181,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: 建立與管理多步驟的審查鏈:初次摘要 → 初次程式碼審查 → 評審該次審查 → linter → 程式碼異味偵測 → 逐步分析 → 總結 → 評審總結。每一步都會引用它所需的前面步驟的答案。檔案受到監看,所以外部的修改會立即反映出來。 -從 **Tools → AI → CoT Code Review** 執行這條審查鏈(以分頁開啟,或從 Dock 選單以停駐面板開啟):貼上程式碼、填入端點 URL,每一步的答案一到就會出現在選擇器中。 +從 **Tools → AI → CoT Code Review** 執行這條審查鏈(以分頁開啟,或從 Dock 選單以停駐面板開啟):貼上程式碼、填入端點 URL,每一步的答案一到就會出現在選擇器中。每一步都以 POST 送出 JSON `{"prompt": "..."}`,回應本文(以文字讀取)就是那一步的答案。 ### Skill 提示詞編輯器與 Skill Send @@ -189,7 +189,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: |---|---| | ![Skill 提示詞編輯器](../images/skill_prompt_editor.png) | ![Skill Send](../images/skills_send.png) | -定義可重複使用的 skill 提示詞(程式碼解說、程式碼審查),再從專用的分頁或停駐面板選一個、視需要編輯,然後送到 LLM 端點。*兩者都是送出前的狀態——拍攝這些截圖時沒有連線到任何端點。* +定義可重複使用的 skill 提示詞(程式碼解說、程式碼審查),再從專用的分頁或停駐面板選一個、視需要編輯,然後送到 LLM 端點:以 POST 送出裝著提示詞的 JSON `{"code": "..."}`,回應本文照原樣顯示。*兩者都是送出前的狀態——拍攝這些截圖時沒有連線到任何端點。* --- diff --git a/architecture_explore.md b/architecture_explore.md index f258df87..fc5f7eaf 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 135 個 `test_*.py`、2453 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2458 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 04c16929..190438d1 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -116,3 +116,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: `images/ai_code_review.png` redone (Method: POST); the three READMEs; `architecture_explore.md` (test count). - **Files**: `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `test/test_utils/test_ai_code_review_client.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `images/ai_code_review.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-13 · 2026-09-25 · Say what CoT Code Review and Skill Send send · #docs #ai #readme + +- **What**: the READMEs told the user to give CoT Code Review and Skill Send an endpoint URL, but not what reaches it, and the two differ from each other and from AI Code Review (U-20260925-12): CoT Code Review POSTs the JSON `{"prompt": ...}` once per step and takes the response body, as text, as that step's answer; Skill Send POSTs the JSON `{"code": ...}` holding the prompt and shows the response body as it is. Anyone writing the endpoint had to read the code. +- **Change**: the CoT Prompt Editor and Skill Send paragraphs of the three READMEs say so. The formats are unchanged: renaming Skill Send's `code` key would break the endpoints already written for it. +- **Tests**: new `test_ai_request_formats.py` (5) pins what the READMEs now promise: AI Code Review sends the form field `code` with POST and PUT and the URL alone with GET and DELETE; Skill Send posts `{"code": }`. CoT's `{"prompt": ...}` is already checked in `test_cot_session_reuse.py`. They pass as the code stands; they are there so the documented formats do not drift. +- **Result / numbers**: 2458 tests in 136 files. `ruff check` clean. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `test/test_utils/test_ai_request_formats.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6c172633..9d9acb66 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-13 | 2026-09-25 | Say what CoT Code Review and Skill Send send | #docs #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-12 | 2026-09-25 | AI Code Review sends the code by default | #fix #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-11 | 2026-09-25 | AI panels no longer suggest an endpoint they refuse | #fix #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-10 | 2026-09-25 | JWT, Query and URL tools in the fixed-pitch font | #fix #tools #ui | [2026-09-c](2026-09-c.md) | @@ -415,4 +416,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 12 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 13 | diff --git a/test/test_utils/test_ai_request_formats.py b/test/test_utils/test_ai_request_formats.py new file mode 100644 index 00000000..a04bf6b3 --- /dev/null +++ b/test/test_utils/test_ai_request_formats.py @@ -0,0 +1,72 @@ +"""What the AI panels send, as the README tells whoever writes the endpoint. + +AI Code Review: the form field ``code`` in a POST or PUT body. Skill Send: the +JSON ``{"code": ...}`` holding the prompt. (CoT Code Review's JSON +``{"prompt": ...}`` is checked in ``test_cot_session_reuse.py``.) +""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest + +from pybreeze.pybreeze_ui.connect_gui.url import ai_code_review_gui +from pybreeze.pybreeze_ui.extend_ai_gui.skills import skills_send_gui + +_URL = "https://llm.example.com/api" + + +class _Response: + ok = True + status_code = 200 + reason = "OK" + text = "fine" + + +class _Session: + """Records what is sent; used as ``public_session()`` or as its context.""" + + def __init__(self) -> None: + self.sent: list[tuple[str, dict]] = [] + + def __enter__(self): + return self + + def __exit__(self, *_exc) -> None: + """Nothing to close.""" + + def request(self, method, _url, **kwargs): + self.sent.append((method, kwargs)) + return _Response() + + def post(self, url, **kwargs): + return self.request("POST", url, **kwargs) + + +@pytest.fixture() +def session(monkeypatch): + recorder = _Session() + for module in (ai_code_review_gui, skills_send_gui): + monkeypatch.setattr(module, "validate_url", lambda url: url) + monkeypatch.setattr(module, "public_session", lambda: recorder) + monkeypatch.setattr(module, "read_capped_text", lambda response: response.text) + return recorder + + +@pytest.mark.parametrize("method", ai_code_review_gui.METHODS_WITH_A_BODY) +def test_ai_code_review_sends_the_code_as_a_form_field(session, method): + ai_code_review_gui.ReviewRequestThread(method, _URL, "print(1)").run() + assert [(sent, kwargs["data"]) for sent, kwargs in session.sent] == [(method, {"code": "print(1)"})] + + +@pytest.mark.parametrize("method", ["GET", "DELETE"]) +def test_ai_code_review_sends_the_url_alone_otherwise(session, method): + ai_code_review_gui.ReviewRequestThread(method, _URL, "print(1)").run() + assert "data" not in session.sent[0][1] and "json" not in session.sent[0][1] + + +def test_skill_send_posts_the_prompt_as_json_code(session): + skills_send_gui.RequestThread(_URL, "Explain this code").run() + assert [(method, kwargs["json"]) for method, kwargs in session.sent] == [("POST", {"code": "Explain this code"})] From dfd457d0f0e3abec57f41a296f8bbcb299c99582 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:37:00 +0800 Subject: [PATCH 065/166] Send no empty code from the AI review panels, and the code as pasted --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 2 + .../extend_traditional_chinese.py | 2 + .../connect_gui/url/ai_code_review_gui.py | 6 ++- .../code_review/cot_code_review_gui.py | 8 +++- test/test_utils/test_ai_code_review_client.py | 39 +++++++++++++++++++ test/test_utils/test_ai_gui_lifecycle.py | 17 ++++++++ test/test_utils/test_cot_session_reuse.py | 1 + 10 files changed, 85 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index fc5f7eaf..3668e3b3 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -458,7 +458,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2458 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2462 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 190438d1..d6365b6f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -125,3 +125,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2458 tests in 136 files. `ruff check` clean. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `test/test_utils/test_ai_request_formats.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-14 · 2026-09-25 · AI Code Review and CoT Code Review send no empty code, and the code as pasted · #fix #ai #i18n + +- **What**: with an empty code box, CoT Code Review still ran its whole chain, eight requests to the endpoint, each asking for a review of no code; AI Code Review sent an empty `code` field with POST or PUT. Neither said anything. AI Code Review also sent the code stripped, so a selection taken from inside a function lost the indentation of its first line only, and arrived inconsistently indented. +- **Fix**: CoT Code Review's `start_sending` warns "Paste the code to review first." (請先貼上要審查的程式碼。, `cot_gui_error_no_code`) and starts nothing when the box holds only whitespace; AI Code Review's `send_request` says the same in its response panel (`ai_code_review_gui_message_paste_code`) for a method that carries the code, while GET and DELETE, which send the URL alone, need none. AI Code Review sends the code as pasted (`exact_text`), as CoT Code Review already did. +- **Tests**: `TestCoTWithoutCode` in `test_ai_gui_lifecycle.py`; `test_no_code_is_not_sent_in_a_body`, `test_a_method_without_a_body_needs_no_code` and `test_the_code_goes_as_pasted` in `test_ai_code_review_client.py`. The three about the fixed behaviour failed before it. Two existing tests that sent from an empty box (`_sending_gui` in `test_cot_session_reuse.py`, `test_however_the_request_ended`) now paste code first, as a user would. With the AI panel, language and request-format tests, 121 pass. +- **Result / numbers**: 2462 tests in 136 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ai_code_review_client.py`, `test/test_utils/test_ai_gui_lifecycle.py`, `test/test_utils/test_cot_session_reuse.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 9d9acb66..0577333d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-14 | 2026-09-25 | AI Code Review and CoT Code Review send no empty code, and the code as pasted | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-13 | 2026-09-25 | Say what CoT Code Review and Skill Send send | #docs #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-12 | 2026-09-25 | AI Code Review sends the code by default | #fix #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-11 | 2026-09-25 | AI panels no longer suggest an endpoint they refuse | #fix #ai #readme | [2026-09-c](2026-09-c.md) | @@ -416,4 +417,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 13 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 14 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index b53d459b..ae79dab5 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -292,6 +292,7 @@ "ai_code_review_gui_button_accept_response": "Accept Response", "ai_code_review_gui_button_reject_response": "Reject Response", "ai_code_review_gui_message_enter_valid_url": "Please enter a valid URL", + "ai_code_review_gui_message_paste_code": "Paste the code to review first.", "ai_code_review_gui_message_url_already_recorded": "This URL is already recorded, still sending request...", "ai_code_review_gui_message_new_url_recorded": "New URL recorded, sending request...", "ai_code_review_gui_message_unsupported_http_method": "Unsupported HTTP method", @@ -368,6 +369,7 @@ "cot_gui_button_send": "Start Sending", "cot_gui_warning_title": "Warning", "cot_gui_error_no_url": "Please enter the API URL first!", + "cot_gui_error_no_code": "Paste the code to review first.", "cot_gui_error_sending": "Error sending:", # Skills GUI "skills_error_status": "Error: {status_code}\n{text}", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 0d963b8b..3dbd29f9 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -282,6 +282,7 @@ "ai_code_review_gui_button_accept_response": "接受回應", "ai_code_review_gui_button_reject_response": "拒絕回應", "ai_code_review_gui_message_enter_valid_url": "請輸入有效的網址", + "ai_code_review_gui_message_paste_code": "請先貼上要審查的程式碼。", "ai_code_review_gui_message_url_already_recorded": "此網址已被紀錄,仍然送出請求...", "ai_code_review_gui_message_new_url_recorded": "新網址已紀錄,正在送出請求...", "ai_code_review_gui_message_unsupported_http_method": "不支援的 HTTP 方法", @@ -358,6 +359,7 @@ "cot_gui_button_send": "開始傳送", "cot_gui_warning_title": "警告", "cot_gui_error_no_url": "請先輸入 API URL!", + "cot_gui_error_no_code": "請先貼上要審查的程式碼。", "cot_gui_error_sending": "傳送失敗:", # Skills GUI "skills_error_status": "錯誤:{status_code}\n{text}", diff --git a/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py b/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py index 8b8b76e9..affee51a 100644 --- a/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py +++ b/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py @@ -248,7 +248,8 @@ def send_request(self): return url = self.url_input.text().strip() method = self.method_box.currentText() - code_content = exact_text(self.code_input).strip() + # As pasted: stripped, the first line of a selection lost its indent + code_content = exact_text(self.code_input) if not url: self.response_panel.setPlainText( @@ -258,6 +259,9 @@ def send_request(self): self.response_panel.setPlainText( self.word_dict.get("ai_code_review_gui_message_unsupported_http_method")) return + if method in METHODS_WITH_A_BODY and not code_content.strip(): + self.response_panel.setPlainText(self.word_dict.get("ai_code_review_gui_message_paste_code")) + return # 這個 URL 之前送過嗎 / Has this URL been sent before? if self.record_url(url): diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py index 3b33545a..1bf33e1c 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py @@ -83,6 +83,12 @@ def start_sending(self): word = language_wrapper.language_word_dict QMessageBox.warning(self, word.get("cot_gui_warning_title"), word.get("cot_gui_error_no_url")) return + code = exact_text(self.code_paste_area) + if not code.strip(): + # Every step of the chain would have gone out about no code at all + word = language_wrapper.language_word_dict + QMessageBox.warning(self, word.get("cot_gui_warning_title"), word.get("cot_gui_error_no_code")) + return # The URL is checked by the worker, which reports a refusal as the # "error" answer: checked here too, its DNS lookup froze the IDE. @@ -99,7 +105,7 @@ def start_sending(self): # 啟動傳送 Thread self.send_button.setEnabled(False) - self.thread = SenderThread(files=self.files, code=exact_text(self.code_paste_area), url=url) + self.thread = SenderThread(files=self.files, code=code, url=url) self.thread.update_response.connect(self.handle_response) self.thread.finished.connect(self._enable_send) self.thread.start() diff --git a/test/test_utils/test_ai_code_review_client.py b/test/test_utils/test_ai_code_review_client.py index f64888e6..7436e3e6 100644 --- a/test/test_utils/test_ai_code_review_client.py +++ b/test/test_utils/test_ai_code_review_client.py @@ -116,6 +116,44 @@ def request(sent_method, *_args, **_kwargs): assert sent == [method] # sent with the method chosen in the panel return answered, failed + def test_no_code_is_not_sent_in_a_body(self, client, monkeypatch): + # An empty code field went out, and the answer was about nothing + started: list = [] + monkeypatch.setattr(ai_code_review_gui, "ReviewRequestThread", lambda *args: started.append(args)) + client.url_input.setText(_A_URL) + client.code_input.setPlainText(" \n") + + client.send_request() + + assert started == [] + assert client.response_panel.toPlainText() == client.word_dict.get( + "ai_code_review_gui_message_paste_code") + + def test_a_method_without_a_body_needs_no_code(self, client, monkeypatch): + started: list = [] + monkeypatch.setattr(ai_code_review_gui, "ReviewRequestThread", lambda *args: started.append(args) or _Silent()) + monkeypatch.setattr(client, "record_url", lambda url: True) + client.url_input.setText(_A_URL) + client.method_box.setCurrentText("GET") + + client.send_request() + + assert [args[0] for args in started] == ["GET"] + client.request_thread.wait(5000) + + def test_the_code_goes_as_pasted(self, client, monkeypatch): + # It was stripped: the first line of a selection from inside a function lost its indent + started: list = [] + monkeypatch.setattr(ai_code_review_gui, "ReviewRequestThread", lambda *args: started.append(args) or _Silent()) + monkeypatch.setattr(client, "record_url", lambda url: True) + client.url_input.setText(_A_URL) + client.code_input.setPlainText(" total = 1\n return total\n") + + client.send_request() + + assert started[0][2] == " total = 1\n return total\n" + client.request_thread.wait(5000) + def test_a_new_panel_sends_the_code(self, client): # It started on GET, which sends no body: the code pasted went nowhere assert client.method_box.currentText() in ai_code_review_gui.METHODS_WITH_A_BODY @@ -270,6 +308,7 @@ def test_however_the_request_ended(self, client, monkeypatch): monkeypatch.setattr(ai_code_review_gui, "ReviewRequestThread", _Silent) monkeypatch.setattr(client, "record_url", lambda url: True) client.url_input.setText(_A_URL) + client.code_input.setPlainText("print(1)") client.send_request() assert not client.send_button.isEnabled() diff --git a/test/test_utils/test_ai_gui_lifecycle.py b/test/test_utils/test_ai_gui_lifecycle.py index 72242496..3dd3cfff 100644 --- a/test/test_utils/test_ai_gui_lifecycle.py +++ b/test/test_utils/test_ai_gui_lifecycle.py @@ -139,3 +139,20 @@ def test_the_warning_speaks_the_ide_language(self, qapp, monkeypatch): assert shown == [("警告", "請先輸入 API URL!")] gui.deleteLater() + + +class TestCoTWithoutCode: + def test_nothing_is_sent_and_the_user_is_told(self, qapp, monkeypatch): + # The whole chain, eight requests, ran on an empty box + from pybreeze.pybreeze_ui.extend_ai_gui.code_review import cot_code_review_gui as cot_mod + shown: list = [] + monkeypatch.setattr(cot_mod.QMessageBox, "warning", lambda *args: shown.append(args[2])) + gui = CoTCodeReviewGUI() + gui.url_input.setText("https://llm.example.com/api") + gui.code_paste_area.setPlainText(" \n\t\n") + + gui.start_sending() + + assert gui.thread is None + assert shown == [cot_mod.language_wrapper.language_word_dict.get("cot_gui_error_no_code")] + gui.deleteLater() diff --git a/test/test_utils/test_cot_session_reuse.py b/test/test_utils/test_cot_session_reuse.py index 67584b65..30479dc3 100644 --- a/test/test_utils/test_cot_session_reuse.py +++ b/test/test_utils/test_cot_session_reuse.py @@ -263,6 +263,7 @@ def _sending_gui(monkeypatch): monkeypatch.setattr(code_review_thread.SenderThread, "start", lambda self: None) gui = CoTCodeReviewGUI() gui.url_input.setText("https://review.example/api") + gui.code_paste_area.setPlainText("print('x')") return gui From ef5b3751d2057b4efc092a7a1934fb15e5ad7027 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:38:19 +0800 Subject: [PATCH 066/166] Describe what the AI review panels send in the architecture map --- architecture_explore.md | 3 ++- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 3668e3b3..66c03a2e 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -266,7 +266,7 @@ extend_ai_gui/ ├── code_review/ │ ├── cot_chain.py 接線表(純邏輯,無 Qt):哪步引用哪步 │ ├── code_review_thread.py SenderThread(QThread):跑八步審查鏈 -│ └── cot_code_review_gui.py UI(工具 → AI 的分頁與 dock);URL 只由 worker 驗證,UI 執行緒不查 DNS;每次送出先清掉上一輪的回覆;關閉時請審查停在目前這一步,交給 let_run_out(),不等 +│ └── cot_code_review_gui.py UI(工具 → AI 的分頁與 dock);URL 只由 worker 驗證,UI 執行緒不查 DNS;沒貼程式碼就不送(整條鏈八個請求都會白跑);每次送出先清掉上一輪的回覆;關閉時請審查停在目前這一步,交給 let_run_out(),不等 ├── prompt_edit_gui/ │ ├── prompt_editor_widget.py 共用編輯器(QFileSystemWatcher 熱更新,watcher 以編輯器為 parent、關閉時停止監看;有未存編輯時,外部改動、「重新載入」和切換模板都先問;還沒有檔案的模板只用 placeholder 說明,存檔不會把說明寫進去) │ ├── cot_prompt_editor_widget.py 8 個 CoT 模板的檔案清單+語言鍵 @@ -313,6 +313,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 - 請求走 `ReviewRequestThread(QThread)`,只有 `answered` / `failed` 兩個 signal 碰 UI(和 `SkillsSendGUI` 同一套);送出中再按不會重送;`closeEvent` 不等它,交給 `thread_keeper.let_run_out()`:斷開它和面板的連線、留著參考直到它結束(等它會讓 IDE 凍住最長一個讀取逾時) - `urls.txt` 只存 URL 的 SHA-256 指紋(`url_fingerprint()`):API URL 可能帶權杖,依 CLAUDE.md 要當憑證看待;舊版留下的明文檔會在下次送出時改寫成指紋 +- 方法預設 POST(`DEFAULT_METHOD`);POST/PUT(`METHODS_WITH_A_BODY`)把程式碼照貼上的樣子放進本文的表單欄位 `code`,程式碼是空的就不送、在面板說明;GET/DELETE 只送 URL - 非 2xx(含不跟隨的轉址)走 `failed`,狀態碼寫進面板,不會只留空白 - 接受/拒絕只在收到回答(`answered`)後可按,每個回答只能評一次;Send 按鈕由執行緒的 `finished` 恢復,請求不論怎麼結束都回得來 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index d6365b6f..c0261d42 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -134,3 +134,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2462 tests in 136 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ai_code_review_client.py`, `test/test_utils/test_ai_gui_lifecycle.py`, `test/test_utils/test_cot_session_reuse.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-15 · 2026-09-25 · The architecture map on what the AI review panels send · #docs #ai + +- **What**: U-20260925-12 and U-20260925-14 changed what AI Code Review and CoT Code Review send but updated only the test count in `architecture_explore.md`, which the project's rules ask to keep current in the same change. +- **Change**: its `url/ai_code_review_gui.py` section says the method starts on POST (`DEFAULT_METHOD`), that POST and PUT (`METHODS_WITH_A_BODY`) carry the code as pasted in the form field `code` and send nothing when it is empty, and that GET and DELETE send the URL alone; the `cot_code_review_gui.py` line says an empty box sends nothing. +- **Files**: `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0577333d..69ba86c4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-15 | 2026-09-25 | The architecture map on what the AI review panels send | #docs #ai | [2026-09-c](2026-09-c.md) | | U-20260925-14 | 2026-09-25 | AI Code Review and CoT Code Review send no empty code, and the code as pasted | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-13 | 2026-09-25 | Say what CoT Code Review and Skill Send send | #docs #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-12 | 2026-09-25 | AI Code Review sends the code by default | #fix #ai #readme | [2026-09-c](2026-09-c.md) | @@ -417,4 +418,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 14 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 15 | From cf07f9abd58beb117b51e493eb8ffb0e034aba15 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:43:14 +0800 Subject: [PATCH 067/166] Word the JWT and status hand-over buttons alike and plainly --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/tool_header_analyzer.png | Bin 69445 -> 76730 bytes images/tool_response_inspector.png | Bin 79629 -> 86415 bytes .../extend_multi_language/extend_english.py | 4 ++-- .../extend_traditional_chinese.py | 4 ++-- test/test_utils/test_language_parity.py | 11 +++++++++++ 8 files changed, 28 insertions(+), 6 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 66c03a2e..8b7f4f6e 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2462 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2464 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index c0261d42..812fe5c2 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -141,3 +141,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: its `url/ai_code_review_gui.py` section says the method starts on POST (`DEFAULT_METHOD`), that POST and PUT (`METHODS_WITH_A_BODY`) carry the code as pasted in the form field `code` and send nothing when it is empty, and that GET and DELETE send the URL alone; the `cot_code_review_gui.py` line says an empty box sends nothing. - **Files**: `architecture_explore.md` - **Open items**: none. + +## U-20260925-16 · 2026-09-25 · One wording for the JWT hand-over, and a status button that says what it does · #fix #tools #i18n + +- **What**: the same hand-over to the JWT decoder read "Open token in JWT decoder" (在 JWT 解碼器開啟權杖) on Header Analyzer and "Open JWT in decoder" (在解碼器開啟 JWT) on Response Inspector. Response Inspector's status button read "Open status in reference" (在參考開啟狀態碼), naming no tab: the HTTP Status tab looks the code up. +- **Fix**: Header Analyzer's button reads "Open JWT in decoder" (在解碼器開啟 JWT), like Response Inspector's; the status button reads "Look up the status code" (查詢狀態碼). +- **Tests**: `test_the_same_action_reads_the_same_on_every_tab` and `test_a_hand_over_button_does_not_name_a_tab_that_is_not_there` in `test_language_parity.py` (`TestLabelsAreTold`); both failed before the change. With the header analyzer and response inspector tests, 114 pass. The full suite at `237d60d` passed (2443 passed, 15 skipped). +- **Result / numbers**: 2464 tests in 136 files. `ruff check` clean. +- **Docs**: `images/tool_header_analyzer.png` and `images/tool_response_inspector.png` redone at 900×720 under dark_amber with the same input, rebuilt from the images; rendered inside a container like the other images redone since U-20260924-313, so their content sits 2 pixels further up and left than in the old top-level grabs. +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `images/tool_header_analyzer.png`, `images/tool_response_inspector.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 69ba86c4..d72debe7 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-16 | 2026-09-25 | One wording for the JWT hand-over, and a status button that says what it does | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-15 | 2026-09-25 | The architecture map on what the AI review panels send | #docs #ai | [2026-09-c](2026-09-c.md) | | U-20260925-14 | 2026-09-25 | AI Code Review and CoT Code Review send no empty code, and the code as pasted | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-13 | 2026-09-25 | Say what CoT Code Review and Skill Send send | #docs #ai #readme | [2026-09-c](2026-09-c.md) | @@ -418,4 +419,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 15 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 16 | diff --git a/images/tool_header_analyzer.png b/images/tool_header_analyzer.png index 7d86e977f1526d11f1442f0e25b9a3bf3735aa06..72b03575164e166840c641bbadb496a98b5e28af 100644 GIT binary patch literal 76730 zcmbTe1yCIA@;!_M65QQ_26xxs0fGj13GVLhp5O$B;O_1g0t9!5;O-9JEP3C1@9#?0 zzrL-csA6Ykclzn4`}8?y2$ho&M}WnJ1p@;^koX{?00ssb00ssz`UV>K%@2w;E8qu= z%?CAmFfha((0}lF8bmxWu(x0mB7%x8sRv8WF6dKtuTPGLu3|pN9$1Bdv-(hq?uw#_ z_V`i4hP~Yp^$IKafYp$#p!i+;)pseSP-Nr~O3GFmtvDy90xXmdq>lGTL&wqNkz~zy zVc_SFyt0Hp(pipE?TuX0J-Ljx-b8+@j}a7kdBsPmjv>4y0ll)rIs?9hfL;YL`a&OG z-d2=gA|`uzx9EEkGAat_rKIrjP3X&U)KnA!e_aM0P3YyYY{@Vf;+MnZ$$S6xav1b| zX1!Uh&B&NWEoIxc7Hhn`3%9%bZit}MvQt8tC&M&2JiQ7Y@?fF5^QMMR7c8fsF0* zeA7G}6|hNDrgH!CO+$9L8?(uCV#wBJPtj&;3~6v#8}Pv7?yuBUn?8ekEjD84&^<$3 z*I3BQJ$JHS8(fAlR2tt?(B3()o2_WWfmIlsJDb>ho41VRb71~TK>#aIXG63%`q-U* zm{M^N-fQAQF`mb=bUJ(|aCh47@|?sAW0tvYbF(GfCFCf#QQ%QH+ymM_#y zsd#;KSxc^X3d)hA>=~Et8wbLC!r4V(fkv@P_hRx>QZXvp$>-oJ!>e7%dky6U{SsyT z8@<=so{d=S9w`CG?OOicmoEIj>-ZLKO-r;|Av8G3gfmp07SKS#iVprHM&GeRGfYxhl_;(npkUY9P z5EIayrgFNXx}_nWZx6j+|EXkhZsT4ea|gX4fDb8;uE&VYsCRIm#CdDLd-LHn{?o2; z#JvqG*y>X-VRsz?50m*({!P1zPOw5@PekE!=hqDH&hA5=_3B(5{hvLyj60XPb)?M9 zsO6UgKGoi7H0PX=V*b(ZBKymh1Tr6E(<=3+%zFA)Fl(ZH=M~cVF>;lQne6Y-@da*m zc<*{9ZcHA?;Bh92=u$Xn#_|Q8oVUX6@Cqz%XJn9H@h%)fMLg-^FT0d-d}FslS6Db| z$zgB0+zHL9J`bz^fl)aLcd#P&Vnsa1~zeSx||p0TzKmYc1G zV;d<8iywzSiS}zQE|~9)tLIKS8(?y^Y5wG^U?Xam>2q?3u)DL~%8})b%bAPCVhctF zgBEo3Ot^RJ;%go6YDAfQH1l??=n6*C5@om+Z3f^UD4oP|!0q|dqJgAM#BEoL?R&$5 z%jEndG<)DwqVdefOs_4nA%-ATDqy(xj`McqQ$w>?D|58@tRYRdFP+V?|$1gmKC&T)zIm= zlGxeVpLR#PSL#h*^3HBOeVJ$X?6Uwq7@yMtJ2_dcq1LU|XPf5ul9uizF3n;H$3eg< z=EGr;GB6-5I3IPD59AA5sXVDJSX}vRG#YFlDU|<}K&xTt#4V{K+rmrS&id=($2o4b zW>8X$qJjZ~n_YO1ud}wp`A&X5U-l#mjOg!OBy+l7m806~>z9N?u =@q~F=)v8Nc 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 diff --git a/images/tool_response_inspector.png b/images/tool_response_inspector.png index affc0c82e6eed4b005064151791918bc67cf5df9..6b63fdfe5b76af63f7cad1a82185804850ba971f 100644 GIT binary patch literal 86415 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

    $7R2O{uN#0I2Wp9Tw)o_nj1e`A0T~pQF55a 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%0AuO7qOgb?yhPewQP&_4i8ThMi%$CVdC?K8 z-)g&cYm?G$@OGv!quSu~1^b@v;72ksq<+{TlWokIi2V+g#;3|QgakI(m3+OYue|#f zBg)kWwua56!_&8MkeK{dW>U1cl9r~LzKzuR$3qt!DBpkZzLX@&S)BMtI~g{rJ)`+s zaI)_rK$K%K!L_1e(PwF!{oydYt!~6q)b?x8HtZ`|=7$^<6|c&m&Um&gLVV&O)k$?>TkBy?AsSyY1W;7j|+g@a3-I zdaKpE9joQlZtE6U`6Y~+X`Ls<*Fc&Qxf{lN81N0*JT@abQ%ym^B*P0ljPRrZRO{W4QQVYzicUas~amK*y{3xyi)sk#cR7KM|G5r|38byp#D4kd+)L&m7o<6Go=LiLES ztL{><5UT#qepytMju-E?fp!e^eEx({P+=rgl*3$xZZdW+Jde&4p4n7>iPZQD^2M16 z=r2-rS<$4WN}d!V8PcOo3813{IDXzUZGT%rUIMTe{AHDH^zZv1>MP^nh9R5j5cX_& zoAss{1BvDw!pAqSCKJB2t$B&xESWD28%MKV4{4c!w_9c*7wxYOC(CCgY5ksIAeyG6 zk^WNLkBm$8s?UAiBB|1ZXWyr}uD+;Ce+4)SC|KaW=f?p_e3kaGqWR&A{uHd7WH+_b zB~iA3DDSTY6!vbc(ITYdcXMk5faL#@NQ(?aN9n&m_0>f!R?{YCKc~DMP^2J4_*`PKf z7Xn*th<_2p-Ss20zy@VlSUkjdih+yswKJC}J+@MMGW_gAsnO1)2B~G=eCXL3mv1O# z@mRec%SGimGxF;EhH-4#++pX0civ@3_g^BaNvC7)^^1e2OEaNs+~*ULd{IURQYOJZ z#UI!%*Uo`0>9#y(CdZ(@Vi3inq>&2leYqwhvHK1-1CPZA2RF5#0G(VH(w-46)PjBo z)6{+w%)iRK^mCvdW!VbDyGc0I0RAstV82O|8(RWG^z`ztHAjFtfXArXuwW)a#`_Y= z;@mTC2EpeK`I3WKaaVJDth#F?m5s+xB}DSe9lx3HM0osZ_{5HZK(^Q)vz8ct;dR}#huD9XJjH&GtCi8Ulm zee=zRSX@l#q=9W>Dzk893o$h)@nFK5ss|UtNFz1O_x;nyD4SiZ! zjUVe{fF@q@eW+tUZ7O0ag9Yc@aGeENj^el6%sIM>W4&Z7XwH=qGZH zHmc14MF9xUq3W!RyACBd^C5R|(dNl_PhgIu&prk^_3(1aIPY7DSYvWhCUOX{0N$TI zE^dgJU8(I}r-aRdXX2Vtut_Kdmb0e&akHAaoo37)Ef_ZgQr9$#@#6Xv`*1d(QS+`M^NFr?$ld+ z7}-J58b*OFra9zAaBCYb5p<5wz)YX*+C20#{}!=K+*Dm~N;uI|-_R`32Sy+TpGSyU zxcLDW&HfObJ5;jQUbtMbo*z6#!iI0?RlJLb&#`T|&E*5tO%mh3nVc#eNmbw(GP387 zqaKqRJOtwPiS36r1dC=MfdtgPuQZ2#@h~v6DsBC3dn>`9ub^qX&ag2?NG3?_0`PJH zEf~jJf28pqW$p=$a+|nq+i(V>qRO@W0W(ORa$SidTxJ_Wv0BGpGJKdHBPVjQ1J%Xm zuOQMit7~{jRn4gBX_f){4(#!Y;ozQz4{%I?eGllh#rh%Nz9&2ro2SnYVgx@2j`Efs zvk?RYO8-Yg##KehItvZ$j;~?8y`*HBk)J%ltDK}XI~&?O5uxNh`b<>&p5aCkUT(I) zPARCXnOHhf(hdJ&>ua+-PO;5eoBDpW2y_su^wX!#k?ccvZ z6siF+yklvyFTIg$)U4<>GLfk<*|fWAKdiS7nRv^Fdev=46PuvOe*~zK#Kv|~OE}N3iOP)iTi8ajTH#~#H!=+Y>E2~KT$5dLg`>jLN|ArMu zDkN4}b|c28q?cEH+(nvaWwpTWl|}}!?iB4Tm6Icb*e=&8(FSlhQoJz|c25>O=P$(R zU^M<_XOusEYP0i9}KST){9`k_dFzPpU+C{&d z-*GI;Nr0Sy(lrSGo34?if~$7U3D*A>aq($T%_%uSrm)O&+(u~+mc`hIj}0SyQM=|D zZuy&j23>t~WiwP03N$ow(k&;GX}lEIHL+J5njy7b!~}O{-%cYvd7Sgd=le*??`MBr z3$k1MEZyKGdE zy=p+=13FF8$f%PPBXU&0#wIbwKQEG2uz&%Tja(Nk`51w2Gz=H!w^C$x`S=_Nbil5O zZ&-A*^_92i$6sL*&@2D$TZYsiF|uf4T`SBi-3DTus(mkdf0M%SJt&dhfEXBICL*i{ zAJmb_Z@i6S8wc8=!>Igtw_4XY#qQ$WL|sIO~90vDK;>{><+r$&=w`Qs6Yl z7(+BIBuoiM()}9~GzyrLidYnw!1zdH`{%rGX&S8y7citFH`!{3^1WGz{K9#QI!Y_n z^lYL<X8iC$3CxgWWrTDMT(=J+jizLegjsRNdmC?0lb zPU0Od74ul&Es>>k-y3`q@d;!H_t4D)&V#5;5HbF#&P4k)xaT&>q9(IZzI@gN;W-k+ zec4|SrjoBU!L)p7H)wG-Fde;T&0mego&O9-Pxg1W>1ZGdZ9kM8;}8fTZA>P7$8_1a>@K;XVhj-c()+?{a} zsx2`bR}Wu$zPn`R0S<^NjQ)ykM z2celopIyk5fQA>j|0v_)JDiW25vdpDEG#S)58r`HGH-M~C>hbV{$x{>vPH}${zPIR^0D_qm``0(HR zcVQ(fGq${xt?u^CM^OdZ4k%+N_D@|U8?_OK#ppgzxG1@kw+ogPV7PCB%^a~R2t|NR zV>6jaBx)F{l9g8WOS$5`w9ymE7x`6*K!LiKqx`=Boff#yvf#6yr|>_oZbWiK1TncG zP;do`58%QE*66krMYF>h^P}eYJOl`JO=Z{>e5oqbXp|q(XqF5~1htB%)yiIzGVF~W zw==ip?bia}Xs{8C+u*m-Lj{xdwkIz{x8k=h@xSU?DjmIlYDA`3`68fl8+yyn2kdb@ zHiUgdY&@FUE;WTdegV~a-Ti^5IOysBKs<&46u{Z~AI@ZIum2=I2?7Pb!ZwBAIF>!I zlfW2-f!MSPHo6AmHkOUA@ig2fV4BcH&H8LdqFC2D5fcOM$1qX-6C-X|UJtytGGL4W zx@1M*O0@|tZv1`#p)$BZs$@k2GZn#9#x3%NlPy6!H|K)aco?zu=76|gjxyo#pDufr-Vor{l*PFZ)yqzHeg%d8Du0ge6w!PD z(hY==_mkv}>_d4j($m#?IpD{f(pb+={{-ys_{N3OR0Z9;P(QT0JXn7X;TQ>xXEX_h z>jM($!*6J+kB-I?QyAL+VuP2d+0O3yexoC@lbHg0>NsQ+``Kt@+Sl9uA^mF8YBJA= z+GtCZxjg24@}Nz|KMeJ?hJwfc@c2dDp|ZH9foV`EMB21~#-Vv*D##l+JKQiPE$Nn6 zN1ght1|NeFvFb!&csqTT=q%I?ce(77@LKE>F{%pSzi}C4G1xaKd!7leWoC zQa`ztXfHl2$@z8IByNDP_S)puAGSrh4-2TCVEG#_B$}vZ*ThTx71}@>!1$kS00}?x z{DiC?ef~IAW8l2s?3Mi>#vOtB;+r^ML;U*rmk__+j3?=)iTQ6~c3DLEcnkvUwhDj| z-Gc{A{2pa4@Y@sr#(*D*ocjlhbbSlKU}s#_69!LZ0>(hlWW%C>iqi8yLB3}7LFm*! z`=(lv{w}$VeIa}L-dYU~Jyi#d#M!62AxyBTdOBh?>=YN>9%qkdLc`{^vEd!0p|a@J zEFnHSveK=8^{x(k`(W-;Nky}w&Oe(AiZ`SpZPvKRJ>PXOTNI2K#ez8lWWB#(pH`~v z@+tQTnqx+B=;N}(jPHF7wdVuFo2fdp-r5`1EicGJmLl~YOY-C-@r*R+Myw9>u}tHA z9m}vwR4Gg&PT+csXLPDHKZV#*Ch*Y@s=B3#oi5-1VWXO8Bgo&aCjVtMtdi4zAb2t0 zrO{B3wieq%XdOMA9&i8Q;CI~t&wWJnd5CW&&F2A=#Lw2TqTclKn5Cx_nC<=7+G~sY z5TdKaooYfq2$@evRT?&&7a7tdc9+5lABg)#^C`E}{Z1<*CPnb*{Q}0*D|DjbqZGNO z_6JIo%(nd&(Dw3APE7$UGZjOo??UeyJ*i;Sp(X*;hS>YQb@9|_OaH_=-f>^0-61j{ zP0hBHPgtQeXhG#0DPU)SOG79qvr(q|O%EROmre=LFd&M^PsYdh zUEa<9aZ0y>BZ#r_FLKT^<<^vhdpON<^`x~x5jc_B&R)PSQ8&U7QR5budeYW(y*O1DeY&_ z(~${lu{{NH@Ep1vF68Kcv@N_0yOZd;RIGr@mJAu*NuYR?9_ZP{@!BIsIjsuIZ#I;K zvmO-Vr0OixZ}|IB>zJ1od5Onkj{{Akje++gP-DrIvGy`rbU%R5#51CCIL^}gTR9L@ zt^FeeD>~BVK7n&?3SH|rU_@F8WnwEVSddcthY2&rfhn-Va*pq9v0lnX&U9(9CHpzR zniRp=3qtg8J?{OHXSEjHR}G)0kJ|wsn%~)f*z!T>#();|Y1kWO8)4;>5VRw`%G7fX zRm2(}-4o&n_A!{4YP&tBIqrl_{Y}?MfM#jQ!741+IL+bzi#n7zlMbh@zXF6d@E@UI zL+wdNQ@w>uzB3LD%$y6}<4!zT{-{+H9iS`fv#v#}9Hax*3`danZBtuOVsP>WHzPv} zmqpO|PiMjAqPj4P6;$dlRf1T6MrK_*IX%2cmrY+`gg+JUyB2*v+`@EH{Udg@*kPAb z?!f0hbneiaUm+&%7|#f~eW-Ms4*EG{qAgIA!HSZ`7ZHONv}rMRS#(Vcf4*SzBNH|B zU8EuK4gg%ZXO{GDDpTHZIBY?P`?99!9q#Y~BB$cO_s_b03F$rU24<)Uk30|dl#4oc zM)8!)c8^PTjO&EFM;n)(k@;A$iQ|A$O!PWPaFjzo#4omv^LDHK6;k7og2rII{L|4f zQuq)IXlqKeoWg4`heK3)5shIcl-}0TW-rYT}bGHA}TYa&)pmYbfzRH}gpvf1>2tr1gmx=qHMhAyT^AF7=Jg;m3ZKGGL z0>=*OMD*ZlG0k=JLpA-b+Br`vGy)kaDw=;gwdfS9?;1Bb;rj~W%-JHaHr|lgbPae4 z_X%?TN<>sNe3`z%%PtqAibDO?ibQt7td3ByJHDT9_U1rsPs`ij32d9>dIFb8KuOO!K1F z5?{K|M;Wk9hpMTIhvSn?M=ZheetF*w|6msRxn2O;FV{@g{pLya_|LOP%FnJ=9H1>) zA2lVJh||OLK?DCbsph54Vdg{92Ob`Lf_%8~hh;>?V;ev(l7x+RVgKoC9fF~{W zFou_};@V`EA*qrgZWZ*-VKMGCs^Amc#UR~cJ->z9fhx*%?Z%u| zzC5zNZ~)-QIZaWDq$CiDwaHB0ipXvaZpMTrBF>zm-^^EC0Vr0~_6fAcZ^!2Wzdf1N z*9W2B^D)zJe#nO~mhwfeqKPJ|#P+miK?TnlHGS{&dGw{~83)nuhTEFGlC zRvhuo@lINOeG$bK!A%Jey`hlmCpNiKKI@HfU5=9De|%0U%&?osqHX$htkD+^gT=AB zMwlsDbmS;#h1tkR#rm%QZZ}01{kv`IdA7F0$UMDuUKQ=KWSk5e7fNVq!Ts@bvrgZf zu8l)b&`A`YMO$6P-Wu$3FQ(_zJ^7dtH$D^5W6p~T7hwD4rE@XCD?d3@>AYuT&p5vh zAid*T=SK>g`RwO;q`~X^ncz_v*1%7a@g@$2UEk*kJv&=lByV48^xbwm#mJg+*-2t> z$aat6XW+Y)2zdF#ddNYU8~bITpDFBMj*sqXXW~1pYvXW1qwaKD5}Ce34x(i`9XLYa z@DCUbRUt2UpQW}+VHgN(DuhJ_zht_4RmmHpyVJMgR;6LKZ1tQ5?ZXEaZ(ae$0-E!6 z%0e}m2IM|9y9eL0-Q)JYj01SRYdj zRrabB<9##+><>9kJMoZZeV&~nDe|8qbOU+)3vZG3M$$rtNKu}>{``XXHAN6ZI@e^E z18#M;?ou*|TIuw~YJ+8?&y}IbkI2^>NR4vBMn}Km;vB*a?>&3de>D8|ct3@(o7o;b zAWP&)M0iWLlGe=$Lm^#IgIx+9H%Elt{DFA&Eg!xpv+8p+wBSK+Bfm++>4+G8t6jSd z9%7x156AvdNTKDiwIP) z#7mAyq5YC>9uoj6EV@g@Lrv1XCP^?w&z7+rI2JaqJsZq%c%B9vjGc)q9Zty9My^f1 z!z&{0)v$Y_`2;$bIbUIRVa3+G-?&6Mxq8^y8f~OJg-)GO))iN~H}pp%X<0G&oe~?i zwyu<&rDGmIzjcf17>5V_=NT#{P7t^yAlQWjvIS4mrIB>CO)~`Ib{&*P3aM7EzxE$$ zpxu3NSu_~RCGUqk8BjxQ6s34nLSPxId zdquH>HVu4y&rTfg4u2cpiX9YAv=5&7IsF(Ht1Efa2S=jnUGq?a1MC69Nw)Gx$Jg~N z81-;$*DuaH@izO8+k?d~pg+BKn*zSdocCHwS*fOFYO&LSR+Hi#@ecv#CL$-}s8nhA zPH#IOu+jQ1a+uwbe~9n)E4Z(9_?4T$6FTSCkS~h{C`iHv&gxE;E6o;W;v}p6!oU2q zF~zPkY$&a5x|FZS&o%M0b}(Wpg=cm1{#O~N{*&5y#cB)(+8{$2QJ8x(su@Ca(Wql0 zDF_GeAvTb7nJ=V@m=sZ~JaNXA<;~mGF^p0w#P{#%iAps~-2KqLSChnY;$Y?t|3Q&N z<^jQNrmMc=N#Wgk$dSQVyqPF3-0IUgN$S9@qzKSNaK&v-h|Ulj5od{dO~og&Pud zBWtjFGh)Dxh8ODSdf=2A=+br2vb$7-`8I7;=XaxFW5>*i^=U(5h_Dr6$n?G^4v#CN zG1Iivt{|Ee+q0QmI`f}IsnXOF_9;$~2b^hD==YI#gb>=tVAqBAjFrvm+mtlpmKa6y z>9Tq}dQ`O_i+7LJa$>$*`kvBFCJi0)!GiPqQ-uTNX5EKxuQz0ZJ!6|^v`b4%eYcEA zvNTZr--gv}3>A>=Yl?hrx{ElpUwG?UFYPJErv-ORs;BRjEc%(X`fjl0m%mpVVHnaj zRNzDMd2`?nBhGXF=GU}toieU}U*e3wiHz~^Qbmf`y*cZ0c}l;P(BWvxcUIL;zud>m zqW5_^3!|9)wjGag+IBAJJpqZzNYvF?R8ddII`NTiO>{$P&srQQh369W%eEDBKZjhC z+b0FI(AoP1oAKpI`ff#ZvzkaG2KIW~0J{K|#~}PYIc~{bZXrDpK5$RKUyQK#X!)hd zLm#@Am1)@tH%kPV>kQNm`u6MP6#E)I>3fsZxDxI+cg3#r_3>dP>U|0H%hBf@OD(3y zFlmD8-Pb5}=ieB2F+I`z9ddL0w67&-?k>a^s6S$}%u$8M$)wv5WokKCDQ$&k8u~`^ z3$LWv&8p~eu{wuD$}FDNY9z>Fr;_{XkI@D|Ay%LP!R06%Mm{tGqgoRGCh?qnqJ9Ce7{8w+12#Vm+Bp zUvEQ-*d*?jpsW~^CMy0doAzCKMe0jJ@>Uh5}lk>+DatFHSJBR+N3R&I;n^3lkS+jwuM| zXY56SsmB_gAM2vpCF}nWYi}79$JVV2Hv~u^1h?P>hY&QlYjB6)fgmBcyM*8x+#9EH zcMtCF?(Pnae3iY=e$RQ&J>MPoj&c8YcXd_GwdS0UtQlvpy~m>)SiWWKh7RQt6aH(L zV?N0K{*M;m^6 zjVe(n{ghE*n>P2SVptT7;616N!Dsoc3=;AZOXg#ZBHUvwB}8VgloI#+6xk-IzrhK{ zW9(3OE6#pnZQR-LhF%R{$dy%XR7W%w$FA+h-4e~w=I`&zvTkv(-s9r?&EdhZ(GC;- z-+5V6biUqRPb_*#QF3Wreu%h{^0D-r@pC{pzA8LR4s9J8t1Y76%H=4bylP7P$jol5 zqNkev5e@G0Bzv}%?VWzK%?zwZ{5H$7ta=#E^KI-gu*cWNoudtGms(=?U9G$;6y3@0 zdPU@7nOg4TcIT8;^}5HjdFOCJ$i_DIM`oa4*Tt!2e}gIokLPHQ6^kVp`$@ANLgcnp z`@(BnclTDGLTK~0wvgKY!yn}9r0W7)8^e(GAlNOiY$KLjZS z?M7Cf`GW;hN!nZ8t3J!#WcpXI{I1e2)`?I3#v3FT?JMT%jVti$tut^1U((Id>@kyz=6`%o-Fjy$pO$P zy3)1gm?h2ro`+wtzjh1G3Y^cJQ|J1RN3^!TV|Nzj`ZIjCda|`~wB^=W%>*WPFV^+k z0|lK{*0?DG6^e>p>(PAK!u5%WYnN!Mrlhf!Z43NpPQCqt5P@tZx$8?tVu>8mc+ZUF z#(6@w*{5TI^R$-nYQm^(d-$STR^0ei!dclq4^MVFpCScu!_uMQ6+n6giP{vRdnElR zzYBIbVNmz^uApOkUJ(t21o}7uRf^SGZeqf{?g4rNCdl(v+wHdL>nfY_kyIw-h6C=C zJOnss5q zy)}M8(&gFgz;j{xkxS2?T+(0)OQ-*9x1n+8JA7bj)sktBuaPCk@K6rAj;vT~N z6?(r`uArSYLJpGYhUdtyajGtSqtq|D@Wh{Pa~#hr4wtBjt_mgNWJW0|8Za#3T~5e* zOm1%0@&{6;aF#vda_1hlEuIP2uR~e|b6|;-L}E%6eL*K`p8fA0C9uX5$0{iE^dNp+ zRxHQ2LVA!2JW!n6sEZt73$TyPz^+U%EW1|4lH4<|A8P*Gu}23XJ4?G57x9Rw?fYZO)lTq$65vevasWXXa_M!kk_ijARbrkFU!OHBvKxWGG*dH$R= zCZhCv`b;8$UB(iQ z_k){sN9nk#8Uq(rZP+DY=ujzTp6>dUhRitZi%C|A`khy+B{vBSgrKwCg zxyIA$9YH1H34tf$#|OTTLV|U14|yTy2WdlWLj~BI!AH}x9gdAZ`JAUCExziZhkCyp zXiueZ1pweUzhh6xBQKHFyuwiY3)^}7j=|G4KD+A}o!6H~_?uiJ8$rdV@Azd`qbsBc z@;hgH$(CY!)bKu(aD?vWd|y#;wD(&(7vH$as#ft{bk|SKa0tDuphP z4;hX@-j2w+h9@ZQaHS#j6{Q$#^g$yffRiF3LyD2{op7dszo2se+^4>d6yxXas@Y|` zKB2C?fI4S^a=oh2Qw%-cWl>32S?zGUVboKqa@G9&K(ztxay?dQfPDCIJtqi*i1qyk zA%;{A=_%Qi7uH%@fS&|9*Ab8K#ZsNFaUkl9J16D#F|gUfgLWpE;XulX81sHk{omW1{<#1| zSWQ$}60QZdB1i?W(m#)a(B6uF z%%kFOoVLDK@9;%c#p)wz1Fl;i!|6fEvHF@rL)#Ax}@|-j;R@SUV84x8j#5 zb<~$9MR{6cEZ>rXcM+AM~a69u4Y2+w$vDgE1x@r3qN-`jVCQB%|Ji45i_9Iu88#e+U*Wb(p zE*D5`iTz1|jJ;e+mOS6A6eafS6BcTJx(~@Es|?!NV)&5)uLx|eAkLR(h1;7K>AJ_8e-FW3#v;V)NTN3Euk51 zak^*>D%OkXydKmGur{j4%4M$7uSf1aV9J__=I7M`Y*k&Hpsvn7>H5ZwzJYMy6~?}U zaKT|jg3^$Lz0E-|N)myvl$bx*h+TgR$CP$r)4aaBtjfYA{F%F>?MH(y6vy(*260$0 zpA;?M_cNDR{I6ZhNr9`vt~otq1&)$L>Y65~dAc_oQneroVvZ8p?XP{-;MJiH*sNC( zvm)1yZw$2u7rJ{L39Ie2HkTT?+JC&MDiSnaW#EN7et5g(1INb&P6eXY1fhTyeBo4CNBN7LCj2=UCNs zya=mn%}A%rBtcAxZ$WsYs=pt7`JU|HKx_rH`fnZQtZ+iY_^i8`A0g)X6gM=5is*8< z2a=SaQ_c_=Q1O1^Qmm|ECu-J5H#sgxLc}|Bf!7;5Ce7ML4u5|OAJLk)}OIp;B& za)VkXZ#l_-I$o&2@`d1X-_FY|IFl9|h!~rY7DQV``+KAT5LF zw%}xUf|5J>j18HW2|oHqzC$`hsd+4`5}IILKFRyG@@ofeA+w}vB{>+CDzT27a3`K9 zPUf}SrO($L8@GjfE+J0G-!=#Q#WCn+PFuQn^9>`!%Blv)XakL_m@PojV~tiDR7j_zEx5#v>yr_-9p2Ylc_%}t3oP7-%;(W zM-y@%@Y)ZL71gMGprmVk&y7uTO6>sSdZI2}vGV;xGLKZ_x2&z}j+El$iUg4z!shvY z7}5r0RZ7jKqja6_nJc*M7)kGIB&H$3V{7)!r@?ZrSpa6}xmIqS5NEeDs=4tB+k-ST zba9&bx^~OYCE~E_p?@0q(riU=wdYLD$TJX|e#u=SMr3+OmMkP8zMr2^5Opz=vV3q8 zS1RVNgu`(bzfwGhv)$`r;!0I{%xET#V|Nk>eQk4QuPWUIp_dT-E*IV{}V{iO}rEFFOdXlekvhCm>F23w>hs-5>%Pz`Q`n@pf$tY?;tQicZMSLYN-l~R z$9we)3D)$pPG$_cotlY>1`wD#NdiU9^IP@3q-7)i;97XwPl2&Wosf;QZT%mLzCHPH zxRMngb+MJP#tOiOJ^BIGXTp&ygJ`>?urS=u6EVf3SlwT$e8I>wQ5-dI>vKJFifHJ3 zs(5CbZNjhLWQUnaJ;`QTE#b~%@{(iu8;g0~L{URn{h7wijbH>z$zfI@7SjNx7{Fgo z&LAwoCuPFhD#w0O`FKIC3{Z|dqmLRAD_umk@$PbR(_NBQ;s|}Cl7n1s;7p^_$S-9Truk| zhK9`KJBFT_b7);Fv$9bO86UIxSEUDCP)42uBwzlH;blJ z+oKv18%a?~*j@Q=k)0MOOJOgUH|_67zJ{|zP8J9hfrBuZ%Z(w$CR%UBgjcTnj{**j z>B-l4Sm~G`9bLgq2+~-IJN_GJ&`OCz{l>Ze2b)RH16^=~dt<-$-cd|MuCW|JL5YfP zOzA7Ut87}HS|bl`@YFE4dHcJ9>^Sg>tx~?On3@#2MkN%ickIA|6F;=QJEbtXlgT!n z-WsMw94dRqG9uUiSZ@;#NtPwdmg6XwuF-6!2CCL}*|<27ao32E(Y(MndIz1EK8MM~ z+RSuL@^2qKwj=cR>c)z_N_yEUx!>$PcFNZQ=z_VK+>~!L7vu7ujeT{9=fB4AYi%Z& zJ1K3U%3n_M(SbpfnD#T4jd50$ymxwF6|_L+l%_OcH}PPn=nHNBb+~yGk&%UVE{Yf_ z`1Of1=Zms7n)aXivqTbyM+Ck5Dj1NYgoEdMFzxD6qX?FP@hf|@v+jkvv|RHMEq$y5 zMQ%F*zw5&b>;{U`=K%yzD@jdtF{6cseE(HD(CmZV#x_|nKt=RWuBY6U}Lqu=EjSE2XW_EpKJtI!F&~G|HFVd3*2KTtcfZaxsG})Eer6-DX;~VE+ z5hYq0z@+~T7p_&x!aJKAbXXo-EtCk#Z`oaPkAF7-fWxWd5b%_XYm}G9Jn!#C&9TNdb~9k_u0`D zOLT<&{LzFE^ed)2+k^$+jq1(hfB25OdBiXXIaty?Lc`s@-7LO%A=%zHAXkaa{V}$A zO1Q=mf(>#$A;V0#P{hwtjRM=~(~`%=+-Qr@JkI?zT2tgsjfrzpDa2sdMllA;rAwcU zZXKuMna&Ie?{^t{2T_%}Gpi3g%WHPS#?_Ri>QMlJGEcDcO;sI7S=RQpNdXh<3;G8m zKMddWd5)NBGg1ORjtOY*r7>oriM&kR7XC+$LYbJ|EhQI!6td%F{*({brI;v%sl~cN zaPIZ%`vMNraT?t>A^22%MdjBwB8%F;}HG zeEx#9#&BKM>>*;ha6i6+`nszLB&^LoD%43}wd3WVxRXd9`g=FAP!4okRVr0Xku{(I zl+$$&4iA~iWaK;_Bae9ZL7@0#qG^%4?;4Rxa>z#|bvM077jWefLRR!!N|}~MX9Qt} zjp00tzxdqu)5?%8;E}r_0i-+M^;b)IiYnl~ORdsgCb3a&l-AX6Ao(Rp9 zKL!1^+F7<+I+4v0mt-rUHC5NUpOJhClEKeziQd$$obaSut_K#>^`|hC*=A_=Mh;DZ z1i{@lbp#&*9avDA&QKI!fQ|HgEoLpGS(!nj*>Ngl?rUXCSZRs1k%@ik1D`hia9< zxE@`BgaZdEn@?mBk5myB75g<2kl(7AvK1ZvVcGg)A;aA+1Mr0MunbqO&w+~VMiJ~H zyvX5U?R4;6wX@o5sV$!VJ-cI<w2SmuUAU51@^VKx& z^DcqJ+mkMM`p-1y$#w1(Xgv@}B*obg1RoMY#32ykV(W}12miFbo{B>NHMPo-5}M9` ze|+KZezE+^Qn*)r8K;*lLxs-%?5f^=rz_95t0$@IdKrX8}Ptq{6pR%jm$PaFhGQ zM`!WeHjCExuXD|fpOyH~I-9O=jt|F5y=LFD<2`~4+$HR)*F8LdKOtXj>R2Z(qyfUC~4);b}dp<-FV10geeI6TrnqAR`xCBgN(?c=PtO3ZQ%KlJUZS; zp15eak)qwe2*;`huED*J>97tyabbIRV_gSzViA3jOY}ulw}E?h|4Iimw|N3Jo` z%VAEp(1BB~v+bob0L!3Swi9q21iA(|5XT!}dhgGcHTq7m2j#dMjXw9eiV=RR&>H*f z<5dlaoY*iy)>iji3M#}9z+4Ym$u=%-V#wVHz9q@lZ5<2jl@Fct%LGEHOzz^jLDrme zUc{pvCMLGE;kP?Tpj*#Wu7z%QR&Anu$Tx(i`jh3ix3@iMnrSI#d$XU1wSgPP*7tpS{V!Herkprbi308(mKb7bRKA<&Q90rCAJU2)ddH?3 zPl9F0Pwg-M>p`U6P-ItTTmvXh5rOY`3#@Jo9*!1Jf`2JG27tr#lDj9Vu;&7RyVqU* z##=jFXR7x3NUG~{W8nFjw?o|h2hv4M{)UU2P=wU3K*?uh@m7Ky9ZtCez8tHUU45zu zQm0>S{)3vMMF$;f6nQnqjvnLM`$I(0Mq8r|p2q`yWxD}1QzvP_xmN3==EH`1gWW;s zlIFGyZ)10vTja8d#lHwG4Q{fVi9WEviv~JUHj^2=mPfW5zi=hQ-(i(knzp|=iEe_S z>5(osZ@QK#@9C4nyci~(tWuRr)8Prmq#g8Ag{ZK<3#~P{)s})r%mF?SiYp0u2k4%( zpBXBo?1$T!g(bjRCWlKc-l@MaKb+wC(Q ziR~PI^9KoUX3D&ZmT%rM+iC(Z*c)y-HMwb7f}}>`HTHGmPoZH=^?c`@jQ)Ehh0Ow2 zw2RdZY|*N&8k6n{^T8{{?N?XM)kV8IlKQyQZ$A{VKAhH1Ul3K-CJhvbhiIY5nJo&a zW>H8>L}~xloE}1yqkZ9E8=GMjpx{Jk1$?8f1*^61c7d?jB$XK7$TIImEYmkU7x)67 zHv5y|Ru4-~&}8wuIe?%Apw-_-_qs>6@6c%Gg>KBs18>bTJhwJe)W6=d8Ngt@XCYqx zeM5_UVhIZq#iTybS}R7Zol7#UymxnvXLXIa+Ip^lq*q4%MIqx*Sw7!}A>T>;?Uee} zAjgj<^58>LS1YaSIW?Kd{MhWBXd9g*=~|*HL@(E=HA8|+X@(eB1dgWo3N7io1d<92 z3>~1~W+X0U)zV1gXj3mF)&2O<{I}~xd`M;Vgx!k$AL&VY!{+8ziuzbEjWZ-Hr4agy znxtEYOT+0pNqSX;>W94r*@{EE4XKsnxQxu>s4xexQUCl9%!p&GQ@>T9p~(9+L0mMF zz){QZwRlwLLa~t}uKhQf1xJNldLmc{)N&8LN`Ty0Zdlo-Z7ZN{i0KLK8w94N|Bv(< zPQF)xoD1nvTDgK*TV2n3?1^r-OM^3WjcAi#b55^zUs@6B2)yg6XINw@`mr{tg;v_L)0fJ(X5eL06 z*ROcal!$+UA{yiCih6NE-ob+g_ki#m&Hnfs>KkIw_!UT2mDD;AJO@s5?)<1Y4MG#L zK*rcqS|6gS5PP1efl@qLVvHmQ!$|>zzM^0{-L3pi@P6X&{~zwE9#)L!Tl?EF>9;x5 zd@F93rky@6NxjY&A7s*(C=@WZ*Toe@W8?FI?Gc9_I)Gx9eu$d~`k_Xw@;lsH2!06y z*8LZ|2{nP?cL_s-Fb#(Sb#zd9q(}oju<>eZTj7mKPqk!>G5;|BM+0+-JjyHy*Kce+o8rQ=zpKWW&n53H*vIyVu#X_g&jQQs(@Fst3s$ewN=vW7DTp87 zrzeV-p5;^E8A2n7Ew%zYLpd9#nKIRfty=a=105gITkDp~tW(q|sbK`EC=nc0<#fOv z+rbmtU*x-ZW{#`w)BN8)me}^;T=WyErSUp!`c#(AsKx>)X`=DAXRw0jO@ufkw_>6d zzm8r4*qS*8k#{^7Nq7$up323OZ-SK){AEJJ*mln+Yb26+O>mx5N^wI1Gfrr7KaB7L zlup#|L$0laYQXl1N_j#((ERZq9a)xzT=O5QujLcK%mxE{pwPjw>58eyM?I8Y4adKk z9(;fT^k_t2S+-<4We`jJ1e|eTew%Aa=|c^<#T56XyB)mq2I*32XziBL^}RRl=DphKDxiCUGrUH_**yic{^0@zww67X%-CDhD~=^RHK#{ zG>CTJlA7Ho9%m5Uj-;Z{Dp&rd8*SpfAJmCyPVQc9z1mgGZ~6vZJT24$SW|H}mvd}~ z=d37e`U_&pM{0zjfJoPUIgHfEE^Dv4Quni5Twh=8Wh2)hJnqm>Bxe@eK!f9?Il$u; zw`{Yy0RT)K_yeDDJbAtxOunbSaR)E|ilMLY(ibd$IX*Rmq?iJ$cZC+L1EhcO-_^|s6Ud#t|b$zo$t4N+BX z9{H8_*ujEOi!nr&4ux}lBdj}|{5pg~o7}aKE*96pjFzO8)TF8UUo@4T9-!{~?_8E> zJWhV>W_d`x9x9JbpXxiGP$WmWaucn&w4Otr2%hh%Sb)3?4#1kqYl|7T&Yc0?HnoQ^ zW9D&`OQHiG1N3U*7a+JOHn4rtZHd{vr35o3tA^pAp5#23TjtIs+3{A+&oSZ{p`PTY z@XN3|h0Q^$;^d**PFf(+STuUbE(XR@*geFvniCoVv)}!Z^6opzbFaFK1_cf#HRBd| zo!ey&T}Lxp=~pdDL;dfUH?h{BPtNBUJ!tC0->i@>vFApi)UuTljGX)G7evgrwNJAy z8Uvn{oy0Zj0HjJ2hMrey(!XT5Jr3v$5gPyrOkxaEVq&d0pJ##;w1{rRA2ESXDf+uX z?o$G*_JvjaPZJRf8$cmDCSIb5uKu(pu!o)JE7U=gxe9F2JEe{LH`0%$jYfUWsY*{2 zO`fW+Soli+W_3~y-~YqvC}bW*ywjh4briETH&>HF3KiwwZ4vqJl-;>XWZwXUsi`qY z5Mwx>TugS+ZFB-06-jA+ozAK{$Ou5r4~TPEr#AC zLHR>%n@{Ro4tYK|QeWlg_jN$D3i0|VbE_a0dB^#ge6#ozA*=Pla;Sz$Vwh)wHPOLK zgS8?7AXk7J8i>yXeL3KFlTi&~4u$jnAiI@(@kC>;qTJ4MSh4aRhrn$$^y|I+oD!m! zl(z>pf9eEwEvlSAAORu&`$Jgb!w7Af z6Y0j*EnXf~w6#*d5ZfrEoU{4}_?2>Ws>asuDu@Dl}mdxkU`&6h5IPArE9_?A8k6pkzItXyN6@qC?FuU#@JREiJQ3mGal&S~sRSp`RP+M^x$3u7_r7INuaX$FLX- z?;pN@4QN)iF9NF7dAK%PxthpJRiks?`(Mw0X$XP=jj&>Vz0U{KZo~K);4@{%+?@@Y zjAGu)=Ah`(r^LA5t}E>Qh?DokLIrS3H_@ZcU-0=x zcoTo%uqR0_H)q3oSF@YIR)cT}vCh*>4cW@ApRo&Yw zY;rV{cGCL1O!$`MP;ZAp1K{^jAa8&<*@H9bp{nR-43WZ@OiN||n|v{`1jv`Or|)o} z#-ER0qVnY^`+!yfur5f6!*OY!;dCccPq4DlXTB1_ON{BhUF^T$jLx8Ghf+5N!lvn+ z{x8Id@h~DqP{QZRiRsGX(03VDZd=zPj+L>IaQv+JH28gcmuWqOM(^@fy*BF$_k`&9 z>~dV@d+?tXeOe=%V>CZ<9YM@Ny){gL0^g&B;x86RW7jdSR;%xr-&U{j2*I03!Wj;}UhR0fdRN2V(wMt$B+KJHv%j!yiV#HU8t}39OBiNaBnx88GQ{)!ho^pYeV=-Um zPskod6}Q|tR9n%|>jn4s8Y4Ij*l~A4JT9sk!vAI$qMiV$u{{O+ofXPSuSt?ds{f^@ zJDIjLj9|@2NE5I+)m?eEvhovwxo^!UVu(gt87HzS?pN|zU^XLY`236N7Kzb&TLAf3 zqml-Y^yVIuZz`Ssr49y>qc+F0lqRh~Z?~j_mc3YS=*r|9eaF{-<|fnGOFn)3?k_CY zxq=ZKD~Jf+{#jVw->i>dA#{=SJhtN9$Rk3NDq6xYb}TSY*Ya4m#?a?@-eTt5B5pM6 z=qR=#6M$Tly#{ge+*+^qG#jQJ zU;((2(~8!NdH~WqXVu}s-&4(c)ouocTqFOC;m+Qi8HYY>pmH+2Cw>K^(oty>CAj+m-m-|hl)T9*N&yPo?bI-|XS2n0( zUGcu)h1WJ(#HdP%80az7d#zYvIlRWCX+vw6cZJV&i1#1khUPKnBl$xFamkXk+uDU2>MTIIQ>7FjBk% zX=ubaAkL~XLDR^PKEH1@hXtbQ7)Y*9cos=PiFK2Nu2mk$`?-OW%D^^>@s<4f=0^iy zCIb8?UfdQ6WrCcmQ4_Y$gHsk=*TeKJjDl^P&Ifh}Hy?7YU( zU46BSuH%32nqa&=GFQDK>(MNa#1VOuM={#pB+(}rAh*U&^oQ6w&+^ImL3*C5i?)uH z^?Ua?Iy^ozkJtk6Kiv_RtYc`uZ)>vzG*;g}>zq%hYr1$B`sN^S=4z}t(1+w;w6DR% zCmX>&%`*o6J7&KqNec8F}T)fe#16Dlk^GU;#C^+x)T63J~&0JHBy}7{DI>WXg2QIAO5?xe%wbhpuoIvNO5;-^KgFbsv2Y0qandwDgfz#}!D~P&>P868hukdws`|9d4*?ln zX+Zwh0;zU3!$yRPwPw}DtRWnFRseXA4OO8d12n=-YI5#-{#K*D>cscUo3eFbG{J!% zM1}~m;JsztT!?3|zj>_P!wN=gD)|9J%{!)wYBsZKWAG^fYQ$qDqHT4_M96Q;nk}Gwr04h;Y!~RdF_@xaC!T~k%{J4JeLE0K6l65w%&XY zAfN7?$xZC|2 zYBbHQ+g*KM5P~M$s1`aQctMe^*zWCr#fM?MN%#&ZTd~XI=E$q9!lZ6=>1f05?oBQR z*I?2NT`PkI)5%RDCI^WVS>?M*`lM?$i%^Wmzc}>0I@e`+eAK+?<`cF;*qc*pIjG~K zB)?&*HfX#Waaqejp8Toe3J7{q`=b^KXhcff*7E2;bB|c z*j!ejaReAP{3%gn=$IA<(mWpb2|RdSl`POwG4!8}^jTSHsMQ2D+D?#~{8 zKzHN4@iG3zGN(jK(oc@_l~Pudg#;`ALh>uDp*De=C7?k9t*%hGZl(+>o)ol@$nwc# zQGXa}upMxk#j@XedXz}_A^gFv`IHl`KRb!M-S*Zb!shK0Bp>8$Osa5T2Rjx2r%kx( z<C2=*ZIj)7iT#-m$ z66HjMzDjz;^Czk$wOb(c&~7?-aG*Zv_i$|yT!RnyCXTnPQ|r`Jmzk{p29#3!))kcG zd5w~6CyKNRe@ed>MBIK0mmy*r0QwT~lwHgH1xmIsuj81s&Bcod_m&Lh-w^*l#upX5 zcD7AGXmtOmRIkp8lSC`R1{gZW8SXr_zB8*NFi-whjQ@4=tcca)Gs%^QB9>eg;CJ~- zj|^x6`YL!QCSEZ5!0t2yXuvXu$@4|q()EV@k30BPe!pNT19J*c(=y}gl$abdH)}A` zOG%8e99n<<*B5!DnXA>d>*2E5sp2?Hi8%CrgdO&n7t8Sx^s%_D=@O9-HBTYBE4Z)4 zp?5C*P3rY-{grfxH7s=7)=c+l7ixBM05D;sRot$gmR5YTtPr|eoKoE%6{D87V~`fz z9+8pkf7?mg`aYx>w(9A%&I&GyIFyT4%>`BKEw;%&zDhnfGGKCoxcom{0sg;imOpd}R$nT*j*Rpr+?s_XAI!ArPplm=P#4 zuyY_KOOCRdNghWv1fZJZ$~Vu7rpORLeh$ttj+nGJqkZW;JhT}mAmtI$9m7bQ#I2%J znaN2TrrXeUzW}o!O_}#7O49r`U4tWE)Hs;E&{4@st-f>ldAbj7_G-;Ln~hp zya9OC7f$Epf?nn$gX#e3 zm@$1UWYX4PnR}q^?OX7ir9d`)3=V!eN6`nx{y;#0S@|BoIqZf=rge{e!GiaWv2zAr zH!xE6G|ca!3{#~HnBha%wJI2Z^b_8CQs=SlrS{`5JDw|9)2H}77}De5N}%5PFEYBG z-sOV7WORd%Z4dz4rlkgvF0{jXFa7pp>SN1GHkjq<2l!RJvGD!5{U@{FJtNouMFI3k8jApegf!cCOy#y*w(X^i?PS=6JHE*lO4F6% zV9qtCVo8DZRzd(kr&GX>sD!v??K3ds>hF|;tg_CxBE7TxKS(!p7qaZ<&5gL(qVi)6 zS5RyL21JHyr=)(-5-`mO=a4RRexi@Pd1UsE-}|%kT&4+67R-Ed4!e+2*%B8UB^e`LE4 zDl_8}9BB4hhcjH_Z_lPex3%qp)7q&a9_{;E+yqYnsoDbiU}T6~5gV_^+Fu6GQ7@R+ zXl^RI9dU#OqWKl=c8jw9g|$NO-%Uy>^!v6f{5k)D%vYfxi0>`x1v^U*BRokc!ZR%9 zUzZjhL0m@oKQW5_XNrLH8leHPl?d5e3Qsx1BXhe~W71LNQ(ceC-agWJ9AZ585R_?U zwg)U>V2jK$C^BPJX}P|B`VQ&-*z1x-bJ&{qsSHHEGv4#CtmF@ThU_pd53k)aC^$oK zXPl9^fY<+klV%^*xN^IDVfM zR?Q7;5a_To?^jOJtP}ObY4*$eKOcc}lvY93uNCe3WnFuT?#|S|WZ;vEhs|FybGE$c z3tSufpKFu-u-to*mMihsixm6Md;Nc!4g-<&2`D6+X26HjK7q*OQR8z%(8MdZw{d<* zIYF|lwn%MEr%bV7eBC7vKRxRWkL@FL^%^*2a5c=$XP)aPWI&*_o1FV9${xmCrygeT z99H_X!iv=HasX5C7oGAnY9fosCKXS!U|Fkx1d)6vxUi(~MulZWNlZQnP zcTwJmsNJ}LRP}Az$J!-yf555m+(p%^G0d*ja%Xh3Nt_D zP2R$a7^^}NmS)BUtmOu4=k^1M{~`e5?+2c zbDs8?B3Jm~hl^SjLv0Y!4V(}k^%Zvv{qqlQ$kW$RTtyLL?OxJ81DyOwTry65==|J&+d3X0w;F+AwRiB#Z7?6K^}lLIbh z-U<>f=Ow3govr^kS`)4Pp4H&_=cMEedw}FMM+X%xce%HcnHU5H5FF25X3`Iev<5&% zRKlni9;J;ntre$gG0*bL{LS+NzfHgsKBPAycg0Hv&~sbWLdFr+ybPKGzJTEW%}^nRioEkL1A~ zL<^mD_qS@fjh&@!(+h9XpzuzkVf~nk!bf-@p zMR0I=@(H%hXQ)bCNWpOU9UZ*Ah4_5x;hb4fV7~;LBbY|lrT$kVWo^0?%+Y@Iio48p za<}eXz#C13v}4@7A8<8V-a6u>J%7J5ubn6pnm21{!Z530;@Kul^W{HIB63V@>s)?d zStK>vM_cIN(#4XmTWuk8o2gQO1nGX*Ds~pXJS>TpaW{DKAu(`5ViuDp`h>t8y6wb) zT!rY2eonkok18(xcJ&CyP>FltKzjbzFxJ)>`d%@uX0srbYppIBd9k#6%B!4bCwP8} zx}JD1aX4Rrdk0tMpuuos|FC?3cP#XSY#z4t1IEDPJ(peV;bII8rkJ_sxpKOjGzRI& zcb<<_pD-*14C!O23YuiJf!kxVlfhm|QxK}9xAw*@36wWn$4EF458hX$s*C9%NQ9L^ zHmYvgdaoX}JL=tJ6AuhxNY)oD_FlRW1%i3{Ok1{*M@Abi6n@xvhY zh8GOw%PLi;fLUGTnm#s!X*2j5p>6mn%K5RHH#zrHp5hOXcTXah#(UwHK-ySLtBB;P zeDsJPVyk>Ukm#r273lyD?=_|Rrt&j^C+fRu0IzK%5m)^3fD_e9mNMq}v0-wm>PO*bSn&I>vE|fwBS&2LGMUPI!;hpqnLE;hxWij-;V0t*}oR7n;GbM7dxhH$R#twW{I z`bKjVfF7cGMUZ{GB)AjXW>d|3#3?{3Ayob1?Y}QvgwJG9I2eQ9$yd&tl*!>XsfCli z$~!}H*~Whj0v;Y+EcI%i!pGKgRpd1+25UTqSgddS#Xh z7?6EIiq~|ZOo|@m_S3O-`+70g#(lgrM)XUC#TCdS@}kv{3~SL!hYYdco8$gUYOTZ5 zwV7jPk3Qq--tAJ6+{Dz%xTg!zw$T~)SZCT`PG>M*Y@2b?-1Sw^$@I^T#1rmqr$!5J zWD4qEOFm=YpGK$#0#nvQK;G7Go?T(9uP$~A+u>2FoOj(;fV_36m}|z0;eC4nm+UN1 zTV+J9f9R$ng}~z=2Cl>>6BkLJ_AC9?xOF0tIH5l0v=H5Hbb5O13Hzp#KBnUC)uh!= z1cn%kI|}(6UXnL!slpmKC@K02U1)aG!ad^ILon3qbCyGPardp)$85a8bW1O8pHseL zd_qP@4g91at|s&&odN;li~n1+NHL-m#kbnx&AL~S8XlX8U7X>|2fXAyKAhY?2hw|4 zIDLG@(9u85zUjpXd?gy!`Rf-cjr4vBG$ZLadwfZiD7j=B`pkDHm^1+y&UalYs5O)9 z*|)y1K!0|w$>%8dC~YZ-uFK5&mg-X*m#D*or=eq{^cqBC!|g(%q|y=C6jIavep7$a zW~yGhv(}&$*LpZa(M*SNA_sAmNc3y6|hf!YM?UUz>1!t!}`KCWPlNUWSGUQwex29WqVPg&K9?I+BEpZ zCuzh5mI6x=pt_F67d)6;Rp2oR&c>lxI(eR&R?Q5ulY!;bE{-p`dRBKteGa0Lo^aS= zdC&Oi#qe=4XKec%_SCE~o* z;9!$UH5=9`DLC-4;?;@h$G3R=OkMZ_>^`L;b<^1H34W-_upOgVk^Q-aD{(bQxgF{! z^-6n*;gV1}>MY9~arMkCY^byvFOeGkY`hP*8idAqwUki|Qc%pw(kV#}`dsw!Vvqo- z?F9$Hj~S7Y#D)!;`GVrKete<~Bb6n8Ldca1Jp@u!G62b0)r+{JdLD7dMBUw^ACtq@P+eo<^wMt8gZQ-Imk zU9YKScCoC(Q#69^98>h6qhcE`u0+|-EXILRQ%-)3(;uXD>$fNv=|`}TwGjsIb)`Wv zJ3ayDT`K1nrWOApyouPHM?Rh))0DJYs@p#mh|pF7>WPUm>uSry#X;|9lt2^SfY7%o z%{T(Q>TyJE<)vl%ko}aP4@70vhXk?=ww&;y%VH9oq3k?wTEqUo*3L5;&Trr2gG7p! zh~5$eVa$jYb?|4j(V~UuH5mlaMNdR8A%f_PGI|No8PO6$^ccO2Ze)ZgQHDGI_uO^X zeR1!LbIx9CuiuMjuV?M@+t2s&UC-KUpSFc~1U=igqF)0W*+zVOs>8(7;`Kx#Eiqmq zwL~a>Yoa8v-aew<_fN`Vw5b`}EafRR&WNGpRAt5p*ZlKj$+Wx5RkU*Bfp%v1K@)Aq z1SBse@9z4y4|X2{E6G^h2kyPn@)?oi-aoHvC7qEQGxB~v78FCE|Fa}c+R^z#dV2K{ zoeLKgy{o<{8k$onQk7cgxNzO6mshdBxcPHj?Y2kTo~`%;jrMas%gvxbFQ3HBx0}C1 zG3w6dT3N^su-x}z*}|LOCZ50I=?t^aCA`gHF&9z3u)q%ACivx{;mZ%}!)K(|YFIem zakb9vno}V?HX^f+UboF|W0d0Wg0-$Hk(*Y|;N?r3%vitsuWhqM1sj{V?lb$nOu14W zSoNDJJ>0T(oz>+<4*PfmW#ZK%rVO9W#8-R!%yhvB6W*uT{3hg8y`$emw@sixmdIDZ>8%B; zqRuStmEwSWYjgVC59dE>*OYAS@rGPwtZ3k`S+k3zySg9@?+WyVMS!VRFjP+R$@%V1 z(Q>-O1NXm|!vjmPl~==*!cF`P?ZF5uWosy-E9*`;$LZF${!lUE!8PZciRTLm)(#NI zX}qFvP-C%Fc!N=?!*7GR8(AYNlJTJG3Ri+{;0s%EMUJOr)W_ameZ(`h`|!7V;P_<4 z>rAA}l@#~@ove|53HXr{_lP&zjama2SESf;o3JP2czB9-qtxoEIt$zjd%n%cI z%w-aN9OSJgFEJx)eD&V8+{7v@MgU@d=r9vzGH+giZ~v>k#YCdD7C7DY-A8E_E2TM* zd#JHpHJoZL_y|7^IAk0DY{>*iIZxhp;9yvgif-+pBM_;A+<5R1}7k{_9 z^FOT;yGDE0CvCn6c&N!+s<760Mtj&ICPzDD$_&OZ%>V3r?n`S2+d8LUhZTm^LOv3LR zZ!`{sJe#)irtQfj=hYN3k~n64p(%~HT2u@xVrL0DnknzMt9a%{2;@sBeh} z5C>-G?&5$cF^x1&XiZNncXaJH_2@0AVTmSo!!~+0XZTE`h$)n^g{|n@N?dEOqTh3~hLF&H_LUIUs=M|_w z+5r`=0d?nUAi&w7_cBp1u@q~gUgxvw>b+a3CfhRQu6}j%Te9w_;bhgI>md`xm0hYl zj$5*|hy5y9DA^GoFP~GRCFfJkcfqam-|cjAzPuKItOtreZ@SqEQOp5kpT;F(G|qog z{r+yxNgj;7j|T1ocLbdH6zGy&QlqbZMVEs+pGTUnp6s}mVV%DyII{Rx5&C=#`F6|t z*3j<=RRe)&Oi7#XV%34_*031zWX`MCK%x5{O{ye`R;|~9H3VPdXNs;`$lD2Smg{U` ziH{f*XULDj6V-jG>60f4d;&F}Ge27bb4#fYm^>r>C}}Z&M)uSq${uI<#OQctCN4&( zyWr{hcuDcZP6b9zY5gFPW?nEUz&9wqxgOc-Ur-mX(7NZWE^rb8t7J}x{haw^k{|JYty*lY>Pxfol`1|WQZ5cHBcu5 zo`LiM)J9IJNm|;S2~I0S9bdN0uU3;fOiKixSn`v^GT$e=`d1hAkj9s4r_~F&dpo|0 zt{1VEJ=(gG@`kNqBNzeZFy*Zkhrqx}U%W)`5QXRFd38+#Jg&JkNw~4f_*fWVS5Kcc z9kV4}zEC2B>dukVa)&Lm-q%*>%65ux%Cc?T9d7Ei5zF&xg;EYBxAY%0zVRzeNO@DJ zAFvz|^R$0hj!fc0funLl{xtN}UY)jc;rZ{no%+1J&x*m?DMr|-22H?)34(&}INThF z9FkaA$PLZ}xmSHa;=Fdi?yFxNamWEN*!Az%9m<%fT3t3XH|6v8@`H#HZqfGRXBySl z%XzYhdOj~kA}co0cuFpHE`7X(u*zt9J_J8siJ-mSK1Z;_IO`8-{<1&16{bubNJ@mX zg6?^xW{;wcSH$0-F67eX<-G)nzWJ=HF}l$(q-J8u8*x+5FY=lIR_|}RWXcgSVTQ5c zv290>iWlNU_fD_#hw1$--F6jv&yxmH^hv&}Wo|XAKqmdP)xE{PzFf}ph&zhqts}lM z1nWEvn-fBH5_B@IUOX&>F1XNkL~EU_I8ZHiV@~pEc?vgX+D0CU7od{TrKzk=0`&Q3 z#r5U`Lk(a$8DD|7K0HWg90jze#_JxBo;1}}pr$I_&3(+|{UTnZ*`X#G-(RK*UG*aO z9fb>vN``g!PwF76Ha)L#!zD=(%WT<-oHTn;rn{#}0!)zD zbbRN@u|@NYl!^>TG<`O+7TkfKw4>(EmoAk>=-To64S*`ES)k+z51=4t{X)DYV)D#{ z0=IzFUJeV+035PU?3C+CP`?i`HYj+dfao4+O(1`H%!63u=sG#NBl@e4gDMvHnT{}ftZ*PqviN4gC52(m&Ido{D(G=OJBci7+_tH)skLa)!mwj7=D_T< zFny$wXQNfJ2`aQnXGF@4)<@p(qGe_^S5T@^Xe%CvYO@9lj7Bo~$HaJA2vOpRn7A>f zjF&0!!Bg~RgOF$-J9bvsjkMX4*CKsGW~R2uv!YB?Xe2(uCaedpc)C?UJ}fQ%2ez)6 zI8Iu4N}zYlAXk+Gvf|=TgFoW~rEC$ORD!F8eQzumUKEcU>s@b^%ozgdk~*lUnKGg^ zym3)ugfNFWH#>_JSCmyUOB&x-b3Tb3cSB#PdYXoN{Z}12rj+uzQ68wd~bfY-Z_#*a`!ko@j$8=~mg29i&X}H~qYS&a^AY}HP45YmNfA~ac z#qEm80+d|XUr*h4IMt#$dzJG*NH!^6?T$)!c3&==A(DnWvEH(Dz*fzi;c|T;DM;Z5 z0C6)1L)J3RG;;L(^h-z|Hda+v3KZSvM!kCsOWi)at6Jf2Su`lV4tDhv)v}M1^CCA{ zwlbVuSE941RXXv62sqUp*qXOe`0g3%^Uv+vG)f(0pHVfw6*3ZfKjia_8GSpL@AMze zD4xCO{<7*6ZO8HvONOPBu!s&~H~A#p*0@=lzEXF>xkL&wS9yo4-&tuDq28WB-cPg+ zX&M8}6v?+5n=@kB{P}cn^cejN4N)LP`(-BQv&8_%+cv*drfYYqSwaCjM z%8I5BqvbF_`6r>DR&c?4$#{&B4`ZMDN4bS7FRO2Qq?Xkvt(5b{zMqpW%NS7mn$bVb zsOp6?;CQw%gPh`=%L8$=^B7lxz|G-srr=pda{5| zhHnQ;Ik67mVh&;#`rrBB_?~@SM@Ac+!^H?bb^V12#<9H3Q9{KLy*9@2+nM}m;6YMV z`h~@kJTC;IZ#cfedJRofI|TVqyZxY9C(dObG|`&i;*;0t0j&-(B^rYBBxZ^q!!M?4 zvSxJPw=cJ63q{YLCBz!jyhTaTcYQCuy8d-Ph9CQ?E1|aR(YiS3_y?(HBTp`L zrwkfB-m)dT8V7rHu(Xyuc7vkM;?oIoNAz@AkZT7**`=u9SYbicKLL}Ld3-}pSiOsx zQAeDb+r+cB6F~^4_)Q9vm`)r2K6rnL!|zO|7Gw55EZ}Cl2Jn>gBle*TjiF2oTFtqH zL`^9N0}~@i^t}Cs=D5`q<~8KMAT!u)FAKeR_f`YU%RbW7K}0vv)V>~nl_(-te-jn_ zH{)Or+^~W?A#t!uhA)~2h%Ena0A%|c0PkM{U~?fQ;28sxrUAmZPmPgC*O}jiSb5oX zUkEQpVg=ntr~a^+3L4u}Z?5;lxl^>=Tr{D0hs7+z(oNj|!csWqeDoU#4Xm*hGWi2F z5zsT;)92QuB*HFM=b?SQNi7dFMTE$@F_#>hm^7$jXHOH^_3_d^M5bfM_AeQ8g_$A$ eKN8a}$Y>~;c-|$^rW0=h0H`Z#Ln{@n-uxGUtD)Wi diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index ae79dab5..9b802070 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -603,7 +603,7 @@ "response_analyze_button": "Analyze", "response_output_label": "Analysis:", "response_open_jwt_button": "Open JWT in decoder", - "response_open_status_button": "Open status in reference", + "response_open_status_button": "Look up the status code", "response_open_headers_button": "Open headers in analyzer", "response_open_body_button": "Open body in JSON format", "response_empty_hint": "Paste a response above, then click analyze.", @@ -630,7 +630,7 @@ "header_analyzer_empty_hint": "Paste a header block above, then click analyze.", "header_analyzer_level_warning": "WARNING", "header_analyzer_level_info": "INFO", - "header_analyzer_open_jwt_button": "Open token in JWT decoder", + "header_analyzer_open_jwt_button": "Open JWT in decoder", # HTTP Header Analyzer — Findings ({header} is the header, {detail} its value) "header_finding_duplicate_header": "{header}: sent {detail} times; the receiver joins the values into one.", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 3dbd29f9..16afc338 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -567,7 +567,7 @@ "response_analyze_button": "分析", "response_output_label": "分析結果:", "response_open_jwt_button": "在解碼器開啟 JWT", - "response_open_status_button": "在參考開啟狀態碼", + "response_open_status_button": "查詢狀態碼", "response_open_headers_button": "在分析器開啟標頭", "response_open_body_button": "在 JSON 格式化開啟內容", "response_empty_hint": "請先在上方貼上回應,再按分析。", @@ -594,7 +594,7 @@ "header_analyzer_empty_hint": "請先在上方貼上標頭,再按分析。", "header_analyzer_level_warning": "警告", "header_analyzer_level_info": "資訊", - "header_analyzer_open_jwt_button": "在 JWT 解碼器開啟權杖", + "header_analyzer_open_jwt_button": "在解碼器開啟 JWT", # HTTP 標頭分析器 — 發現({header} 為標頭名稱,{detail} 為其值) "header_finding_duplicate_header": "{header}:送出 {detail} 次,接收端會把這些值合併成一個。", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index fd692abb..e0c1d131 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -109,6 +109,17 @@ def test_a_dock_is_titled_like_its_tab(self): assert EN[dock] == EN[tab] assert ZH[dock] == ZH[tab] + def test_the_same_action_reads_the_same_on_every_tab(self): + # Header Analyzer said "Open token in JWT decoder", Response Inspector + # "Open JWT in decoder", for the same hand-over + for words in (EN, ZH): + assert words["header_analyzer_open_jwt_button"] == words["response_open_jwt_button"] + + def test_a_hand_over_button_does_not_name_a_tab_that_is_not_there(self): + # "Open status in reference": no tab is called Reference (the HTTP Status tab looks codes up) + assert "reference" not in EN["response_open_status_button"].lower() + assert "參考" not in ZH["response_open_status_button"] + def test_no_string_holds_an_ip_address(self): # The SSH host placeholder's example was a private address (CLAUDE.md: no # hardcoded IPs or hostnames outside documented loopback) From 6513a3003d19d20ac74d94738a7c08a313d3cb02 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:45:40 +0800 Subject: [PATCH 068/166] Show HTTP status classes in the IDE's language --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 6 +++ .../extend_traditional_chinese.py | 6 +++ .../pybreeze_ui/tools_gui/http_status_gui.py | 19 ++++++++- .../tools_gui/response_inspector_gui.py | 4 +- test/test_utils/test_http_status_gui.py | 39 +++++++++++++++++++ test/test_utils/test_language_keys_used.py | 1 + 9 files changed, 84 insertions(+), 5 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 8b7f4f6e..317cc36c 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2464 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2467 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 812fe5c2..0e0b33ad 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -151,3 +151,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: `images/tool_header_analyzer.png` and `images/tool_response_inspector.png` redone at 900×720 under dark_amber with the same input, rebuilt from the images; rendered inside a container like the other images redone since U-20260924-313, so their content sits 2 pixels further up and left than in the old top-level grabs. - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `images/tool_header_analyzer.png`, `images/tool_response_inspector.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-17 · 2026-09-25 · HTTP status classes in the IDE's language · #fix #tools #i18n + +- **What**: the HTTP Status tab and Response Inspector's status section tag each status with its class, and showed it in English whatever the IDE language: `404 Not Found [Client Error]` in the Traditional Chinese IDE, beside Chinese labels and buttons. +- **Fix**: `http_status_gui.status_heading()` writes the heading line for both tabs, the class looked up in the word dictionary under `http_status_category_` (`category_key()`; 資訊, 成功, 重新導向, 用戶端錯誤, 伺服器錯誤, 未知), falling back to the English class. The pure-logic `utils/http_reference/status_codes.py` keeps its English classes. The reason phrase and the description stay as `http.HTTPStatus` words them. +- **Tests**: new `TestTheCategoryInTheIdeLanguage` in `test_http_status_gui.py` (3): in the Traditional Chinese IDE the reference and Response Inspector show 用戶端錯誤 and 伺服器錯誤; every class, and an unregistered code's Unknown, has words in both languages. The first two failed before the change. `test_language_keys_used.py` knows the key prefix. With the HTTP status, response inspector and language tests, 69 pass. +- **Result / numbers**: 2467 tests in 136 files. `ruff check` clean. English output is unchanged, so the README images are too. +- **Files**: `pybreeze/pybreeze_ui/tools_gui/http_status_gui.py`, `pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_http_status_gui.py`, `test/test_utils/test_language_keys_used.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index d72debe7..4755ad6b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-17 | 2026-09-25 | HTTP status classes in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-16 | 2026-09-25 | One wording for the JWT hand-over, and a status button that says what it does | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-15 | 2026-09-25 | The architecture map on what the AI review panels send | #docs #ai | [2026-09-c](2026-09-c.md) | | U-20260925-14 | 2026-09-25 | AI Code Review and CoT Code Review send no empty code, and the code as pasted | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | @@ -419,4 +420,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 16 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 17 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 9b802070..4839ebe7 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -565,6 +565,12 @@ "extend_tools_menu_http_status_dock_title": "HTTP Status", # HTTP Status Reference — Widget "http_status_search_label": "Search by code or keyword:", + "http_status_category_informational": "Informational", + "http_status_category_success": "Success", + "http_status_category_redirection": "Redirection", + "http_status_category_client_error": "Client Error", + "http_status_category_server_error": "Server Error", + "http_status_category_unknown": "Unknown", "http_status_search_placeholder": "404 or not found", "http_status_no_match": "No matching status codes.", # Text Diff — Menu diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 16afc338..e93e2b0d 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -529,6 +529,12 @@ "extend_tools_menu_http_status_dock_title": "HTTP 狀態碼", # HTTP 狀態碼參考 — 介面 "http_status_search_label": "以代碼或關鍵字搜尋:", + "http_status_category_informational": "資訊", + "http_status_category_success": "成功", + "http_status_category_redirection": "重新導向", + "http_status_category_client_error": "用戶端錯誤", + "http_status_category_server_error": "伺服器錯誤", + "http_status_category_unknown": "未知", "http_status_search_placeholder": "404 或 not found", "http_status_no_match": "沒有符合的狀態碼。", # 文字差異 — 選單 diff --git a/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py b/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py index 1b1b3204..979eb363 100644 --- a/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py @@ -7,6 +7,23 @@ from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.http_reference.status_codes import StatusInfo, search +# A status class's word-dict key: this and the class in lower case, underscored +CATEGORY_KEY_PREFIX = "http_status_category_" + + +def category_key(info: StatusInfo) -> str: + """The word-dict key of *info*'s class (``http_status_category_client_error``).""" + return CATEGORY_KEY_PREFIX + info.category.lower().replace(" ", "_") + + +def status_heading(info: StatusInfo) -> str: + """``404 Not Found [Client Error]``, the class in the IDE's language. + + The phrase and the description stay as the standard library words them. + """ + category = language_wrapper.language_word_dict.get(category_key(info), info.category) + return f"{info.code} {info.phrase} [{category}]" + def build_status_text(statuses: list[StatusInfo], empty_message: str) -> str: """Render a list of statuses into a readable block. @@ -19,7 +36,7 @@ def build_status_text(statuses: list[StatusInfo], empty_message: str) -> str: return empty_message lines: list[str] = [] for info in statuses: - lines.append(f"{info.code} {info.phrase} [{info.category}]") + lines.append(status_heading(info)) if info.description: lines.append(f" {info.description}") return "\n".join(lines) diff --git a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py index 994fc235..e741c901 100644 --- a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py @@ -13,7 +13,7 @@ from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.header_analyzer_gui import HeaderAnalyzerGUI -from pybreeze.pybreeze_ui.tools_gui.http_status_gui import HttpStatusGUI +from pybreeze.pybreeze_ui.tools_gui.http_status_gui import HttpStatusGUI, status_heading from pybreeze.pybreeze_ui.tools_gui.json_format_gui import JsonFormatGUI from pybreeze.pybreeze_ui.tools_gui.jwt_decoder_gui import JwtDecoderGUI from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions @@ -31,7 +31,7 @@ def _status_section(analysis: ResponseAnalysis) -> list[str]: return [] word = language_wrapper.language_word_dict status = analysis.status - lines = [word.get("response_status_label"), f"{status.code} {status.phrase} [{status.category}]"] + lines = [word.get("response_status_label"), status_heading(status)] if status.description: lines.append(f" {status.description}") lines.append("") diff --git a/test/test_utils/test_http_status_gui.py b/test/test_utils/test_http_status_gui.py index a5e4167d..60057bf4 100644 --- a/test/test_utils/test_http_status_gui.py +++ b/test/test_utils/test_http_status_gui.py @@ -70,3 +70,42 @@ def test_has_output_actions(self, widget): assert widget.actions.suggested_filename() == "http_status.txt" widget.actions.copy() assert "200 OK" in QApplication.clipboard().text() + + +class TestTheCategoryInTheIdeLanguage: + """The class of a status ([Client Error]) was English in the Traditional Chinese IDE.""" + + @pytest.fixture() + def chinese(self, app, monkeypatch): + from je_editor import language_wrapper + + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + monkeypatch.setattr(language_wrapper, "language_word_dict", pybreeze_traditional_chinese_word_dict) + + def test_the_reference_says_it_in_the_ide_language(self, chinese): + from pybreeze.pybreeze_ui.tools_gui.http_status_gui import HttpStatusGUI + gui = HttpStatusGUI(initial_search="404") + assert "404 Not Found [用戶端錯誤]" in gui.output_edit.toPlainText() + gui.deleteLater() + + def test_the_response_inspector_says_it_too(self, chinese): + from pybreeze.pybreeze_ui.tools_gui.response_inspector_gui import ResponseInspectorGUI + gui = ResponseInspectorGUI() + gui.input_edit.setPlainText("HTTP/1.1 503 Service Unavailable\n") + gui.analyze() + assert "503 Service Unavailable [伺服器錯誤]" in gui.output_edit.toPlainText() + gui.deleteLater() + + def test_every_class_has_words_in_both_languages(self): + from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + from pybreeze.pybreeze_ui.tools_gui.http_status_gui import category_key + from pybreeze.utils.http_reference.status_codes import status_of + + for code in (100, 200, 300, 400, 500, 999): + key = category_key(status_of(code)) + assert pybreeze_english_word_dict.get(key) and pybreeze_traditional_chinese_word_dict.get(key), key diff --git a/test/test_utils/test_language_keys_used.py b/test/test_utils/test_language_keys_used.py index d074b88e..867a286a 100644 --- a/test/test_utils/test_language_keys_used.py +++ b/test/test_utils/test_language_keys_used.py @@ -18,6 +18,7 @@ "ssh_file_viewer_type_", "header_analyzer_level_", # header_analyzer_gui: f"...{finding.level}" "header_finding_", + "http_status_category_", # http_status_gui.CATEGORY_KEY_PREFIX + the class "plugin_browser_", ) From eecbde87911261cf7f4db55efae016c746babcbf Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:48:51 +0800 Subject: [PATCH 069/166] Word the regex and timestamp results in the IDE's language --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 3 + .../extend_traditional_chinese.py | 3 + pybreeze/pybreeze_ui/tools_gui/regex_gui.py | 4 +- .../pybreeze_ui/tools_gui/timestamp_gui.py | 7 +- test/test_utils/test_tool_output_wording.py | 72 +++++++++++++++++++ 8 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 test/test_utils/test_tool_output_wording.py diff --git a/architecture_explore.md b/architecture_explore.md index 317cc36c..4a54df66 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 136 個 `test_*.py`、2467 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 137 個 `test_*.py`、2471 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 0e0b33ad..a5947e56 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -160,3 +160,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2467 tests in 136 files. `ruff check` clean. English output is unchanged, so the README images are too. - **Files**: `pybreeze/pybreeze_ui/tools_gui/http_status_gui.py`, `pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_http_status_gui.py`, `test/test_utils/test_language_keys_used.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-18 · 2026-09-25 · Regex and timestamp results worded in the IDE's language · #fix #tools #i18n + +- **What**: running every tool once in the Traditional Chinese IDE and reading its output found two lines put together in code: the regex tester wrote `group 1: 'GET'`, the English word, for each numbered group, and the timestamp converter `Epoch(秒): 1754208900`, a half-width colon after the Chinese label (U-20260925-02 moved the dictionary to full-width punctuation, but this colon was not in it). The rest (hashes, the diff and its summary, the JWT, header and response reports, the URL parts, cURL) read in Chinese or are code; the bilingual headings (`== 內容 Payload ==`) keep the JWT and HTTP terms on purpose. +- **Fix**: the line formats are dictionary entries: `regex_group_line` (group {index}: {value} / 群組 {index}:{value}), `regex_named_group_line` ({name}: {value} / {name}:{value}) and `timestamp_result_line` ({label}: {value} / {label}:{value}). English output is unchanged. +- **Tests**: new `test_tool_output_wording.py` (4): the regex groups and the timestamp lines in each language, exactly. The two Chinese ones failed before the change. With the regex, timestamp and language tests, 43 pass. +- **Result / numbers**: 2471 tests in 137 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/tools_gui/regex_gui.py`, `pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_tool_output_wording.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 4755ad6b..0dfc88d8 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-18 | 2026-09-25 | Regex and timestamp results worded in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-17 | 2026-09-25 | HTTP status classes in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-16 | 2026-09-25 | One wording for the JWT hand-over, and a status button that says what it does | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-15 | 2026-09-25 | The architecture map on what the AI review panels send | #docs #ai | [2026-09-c](2026-09-c.md) | @@ -420,4 +421,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 17 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 18 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 4839ebe7..380f5d68 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -501,6 +501,7 @@ "timestamp_epoch_seconds_label": "Epoch (seconds)", "timestamp_epoch_millis_label": "Epoch (milliseconds)", "timestamp_iso_label": "ISO-8601 (UTC)", + "timestamp_result_line": "{label}: {value}", "timestamp_error": "Could not convert the value: {error}", "timestamp_empty_hint": "Enter an epoch value or an ISO date-time above.", # Hash Generator — Menu @@ -557,6 +558,8 @@ "regex_match_count_capped": "The first {count} match(es); there may be more:", "regex_running": "Running the pattern…", "regex_no_match": "No matches.", + "regex_group_line": "group {index}: {value}", + "regex_named_group_line": "{name}: {value}", "regex_error": "Regex error: {error}", # HTTP Status Reference — Menu "extend_tools_menu_http_status_tab_action": "HTTP Status Reference Tab", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index e93e2b0d..7a3ce491 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -465,6 +465,7 @@ "timestamp_epoch_seconds_label": "Epoch(秒)", "timestamp_epoch_millis_label": "Epoch(毫秒)", "timestamp_iso_label": "ISO-8601(UTC)", + "timestamp_result_line": "{label}:{value}", "timestamp_error": "無法轉換此值:{error}", "timestamp_empty_hint": "請在上方輸入 epoch 值或 ISO 日期時間。", # 雜湊產生器 — 選單 @@ -521,6 +522,8 @@ "regex_match_count_capped": "前 {count} 個符合,可能還有更多:", "regex_running": "正在執行這個 pattern…", "regex_no_match": "沒有符合項。", + "regex_group_line": "群組 {index}:{value}", + "regex_named_group_line": "{name}:{value}", "regex_error": "正規表示式錯誤:{error}", # HTTP 狀態碼參考 — 選單 "extend_tools_menu_http_status_tab_action": "HTTP 狀態碼參考分頁", diff --git a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py index c4c7b20b..a7af16a2 100644 --- a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py @@ -56,9 +56,9 @@ def build_matches_text(matches: list[MatchResult], no_match_message: str) -> str for index, match in enumerate(matches, start=1): lines.append(f"[{index}] ({match.start}-{match.end}) {match.matched_text!r}") for group_index, value in enumerate(match.groups, start=1): - lines.append(f" group {group_index}: {value!r}") + lines.append(" " + word.get("regex_group_line").format(index=group_index, value=repr(value))) for name, value in match.named_groups.items(): - lines.append(f" {name}: {value!r}") + lines.append(" " + word.get("regex_named_group_line").format(name=name, value=repr(value))) return "\n".join(lines) diff --git a/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py b/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py index 456cf21e..6b850665 100644 --- a/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py @@ -22,10 +22,11 @@ def build_result_text(result: TimestampResult) -> str: :return: display text listing every representation """ word = language_wrapper.language_word_dict + line = word.get("timestamp_result_line") return "\n".join([ - f"{word.get('timestamp_epoch_seconds_label')}: {result.epoch_seconds}", - f"{word.get('timestamp_epoch_millis_label')}: {result.epoch_millis}", - f"{word.get('timestamp_iso_label')}: {result.iso_utc}", + line.format(label=word.get("timestamp_epoch_seconds_label"), value=result.epoch_seconds), + line.format(label=word.get("timestamp_epoch_millis_label"), value=result.epoch_millis), + line.format(label=word.get("timestamp_iso_label"), value=result.iso_utc), ]) diff --git a/test/test_utils/test_tool_output_wording.py b/test/test_utils/test_tool_output_wording.py new file mode 100644 index 00000000..de89f6b9 --- /dev/null +++ b/test/test_utils/test_tool_output_wording.py @@ -0,0 +1,72 @@ +"""Tool output lines in the IDE's language, punctuation included. + +In the Traditional Chinese IDE the regex tester wrote ``group 1: 'GET'`` and the +timestamp converter ``Epoch(秒): 1754208900``: an English word, and a +half-width colon after a Chinese label. +""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.utils.regex_tools.regex_tester import MatchResult +from pybreeze.utils.timestamp_tools.timestamp_converter import convert_timestamp + +_MATCH = MatchResult(matched_text="GET /a", start=0, end=6, groups=["GET", "/a"], named_groups={"m": "GET"}) + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +@pytest.fixture() +def chinese(app, monkeypatch): + from je_editor import language_wrapper + + from pybreeze.extend_multi_language.extend_traditional_chinese import pybreeze_traditional_chinese_word_dict + monkeypatch.setattr(language_wrapper, "language_word_dict", pybreeze_traditional_chinese_word_dict) + + +@pytest.fixture() +def english(app, monkeypatch): + from je_editor import language_wrapper + + from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict + monkeypatch.setattr(language_wrapper, "language_word_dict", pybreeze_english_word_dict) + + +def _regex_lines() -> list[str]: + from pybreeze.pybreeze_ui.tools_gui.regex_gui import build_matches_text + return build_matches_text([_MATCH], "none").splitlines()[3:] + + +def _timestamp_lines() -> list[str]: + from pybreeze.pybreeze_ui.tools_gui.timestamp_gui import build_result_text + return build_result_text(convert_timestamp("1754208900")).splitlines() + + +def test_the_regex_groups_in_chinese(chinese): + assert _regex_lines() == [" 群組 1:'GET'", " 群組 2:'/a'", " m:'GET'"] + + +def test_the_regex_groups_in_english(english): + assert _regex_lines() == [" group 1: 'GET'", " group 2: '/a'", " m: 'GET'"] + + +def test_the_timestamp_lines_in_chinese(chinese): + assert _timestamp_lines() == [ + "Epoch(秒):1754208900", "Epoch(毫秒):1754208900000", "ISO-8601(UTC):2025-08-03T08:15:00+00:00"] + + +def test_the_timestamp_lines_in_english(english): + assert _timestamp_lines() == [ + "Epoch (seconds): 1754208900", "Epoch (milliseconds): 1754208900000", + "ISO-8601 (UTC): 2025-08-03T08:15:00+00:00"] From 1d16e3c03afce70f55f47af5355359f6933c3f63 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:50:25 +0800 Subject: [PATCH 070/166] Put one space after the SSH terminal's [Error] label --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++++++ docs/updates/README.md | 3 ++- pybreeze/extend_multi_language/extend_english.py | 2 +- test/test_utils/test_language_parity.py | 7 +++++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 4a54df66..db0cc88b 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 137 個 `test_*.py`、2471 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 137 個 `test_*.py`、2472 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index a5947e56..ba350bef 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -169,3 +169,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2471 tests in 137 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/tools_gui/regex_gui.py`, `pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_tool_output_wording.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-19 · 2026-09-25 · One space after the SSH terminal's [Error] · #fix #ssh #i18n + +- **What**: the SSH terminal writes a failed connection as its `[Error]` label, a space and the reason (`ssh_command_widget.py`), and the English label was `"[Error] "` with a space of its own, so English showed `[Error] ` with two; the Traditional Chinese `[錯誤]` had none. It was the only dictionary value with a space at either end. +- **Fix**: the English label is `[Error]`. +- **Tests**: `test_no_value_starts_or_ends_with_a_space` in `test_language_parity.py`: no value in either language starts or ends with a space, since the code adds its own separators. It failed on this key before the fix. With the SSH terminal output and login form tests, 77 pass. +- **Result / numbers**: 2472 tests in 137 files. `ruff check` clean. +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0dfc88d8..7c279cdd 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-19 | 2026-09-25 | One space after the SSH terminal's [Error] | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-18 | 2026-09-25 | Regex and timestamp results worded in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-17 | 2026-09-25 | HTTP status classes in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-16 | 2026-09-25 | One wording for the JWT hand-over, and a status button that says what it does | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | @@ -421,4 +422,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 18 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 19 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 380f5d68..c11fe853 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -184,7 +184,7 @@ "ssh_command_widget_status_label_connected": "Connected", "ssh_command_widget_status_label_disconnected": "Disconnected", "ssh_command_widget_log_message_connected": "Connected to {host}:{port} as {user}", - "ssh_command_widget_log_message_error": "[Error] ", + "ssh_command_widget_log_message_error": "[Error]", "ssh_command_widget_log_message_channel_closed": "[Channel closed]", "ssh_command_widget_error_message_reader_failed": "Reader error", "ssh_command_widget_log_message_reader_closed": "Reader closed", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index e0c1d131..dd06d3ec 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -58,6 +58,13 @@ def test_placeholders_match_across_languages(self): } assert not mismatched, f"Placeholder mismatches between languages: {mismatched}" + def test_no_value_starts_or_ends_with_a_space(self): + # The code puts its own space between a label and what follows it: the SSH + # terminal's "[Error] " came out "[Error] " in English only + padded = {key: value for words in (EN, ZH) for key, value in words.items() + if str(value) != str(value).strip(" ")} + assert padded == {} + def test_no_word_is_written_twice(self): # Four Chinese menu entries read "運行 Multi WebRunner 腳本 腳本並寄信" doubled = { From becee81d92c0e55a00727fb703f8787d570894a4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 02:54:59 +0800 Subject: [PATCH 071/166] Put the AI docks in JEditor's AI submenu of the Dock menu --- architecture.md | 4 +- architecture_explore.md | 4 +- docs/updates/2026-09-c.md | 9 ++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/menu/tools/tools_menu.py | 9 ++-- test/test_utils/test_dock_menu.py | 49 +++++++++++++++++++ test/test_utils/test_started_menus.py | 24 +++++++++ 7 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 test/test_utils/test_dock_menu.py diff --git a/architecture.md b/architecture.md index c40033fc..48cc4e37 100644 --- a/architecture.md +++ b/architecture.md @@ -151,7 +151,9 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() `_ignore_next_change`, `_is_modified` and `_on_text_changed()` (a rename puts the unsaved mark back after `rename_self_tab()` clears it), on `EditorMain.close_tab(index)` (overridden to ask a tab's `may_close()` first, and to delete a closed tool tab, which its `removeTab` keeps) and on `CodeEditor`'s `reset_highlighter()`, `load_git_baseline()` and - `start_language_server()` (a rename moves the tab the way `open_an_file` does), on the syntax highlighter + `start_language_server()` (a rename moves the tab the way `open_an_file` does), on `EditorMain`'s + `dock_menu` and its AI submenu `dock_ai_menu` (PyBreeze's AI docks join it; without one they get an + AI submenu of their own, `menu/tools/tools_menu.py`), on the syntax highlighter taking a theme colour key (`warning_output_color`, `diff_modified_marker_color`, in both JEditor's dark and light sets) for a registered keyword's colour (`syntax/syntax_extend.py`), and on `language_wrapper`'s `choose_language_dict` serving English and Traditional Chinese from the exported dict objects diff --git a/architecture_explore.md b/architecture_explore.md index db0cc88b..18bf34f2 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -195,7 +195,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `_WIDGET_FACTORIES: dict[str, Callable]` — widget key → 建構 lambda - `_TAB_ACTIONS: tuple[...]` — (widget key, 主視窗屬性, 選單屬性, action 語言鍵, 分頁標籤鍵) -- `_DOCK_ACTIONS` / `_DOCK_TITLES` — 同一組 widget 也能開成右側 dock(`closing.AskingDock`:關 dock 前先問 widget 的 `may_close()`,有未存變更的提示詞與架構圖編輯器不會被 dock 的關閉鈕直接丟掉) +- `_DOCK_ACTIONS` / `_DOCK_TITLES` — 同一組 widget 也能開成右側 dock(`closing.AskingDock`:關 dock 前先問 widget 的 `may_close()`,有未存變更的提示詞與架構圖編輯器不會被 dock 的關閉鈕直接丟掉);AI 類的 dock 放進 JEditor Dock 選單原有的 AI 子選單(`dock_ai_menu`),沒有才自己建一個 `_register_action()` 有一段關鍵註解:QAction 必須 `setattr` 掛回主視窗,否則 Qt 不持有它、被 GC 後選單項就失效。另一種做法是建構時把選單當 parent(自動化選單工廠、插件選單用這種)。`test_started_menus.py` 在子行程啟動真的 IDE、GC 後走訪整條選單列,任何子選單變空就失敗(JEditor 的兩個字型選單除外:offscreen 平台沒有字型)。 @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 137 個 `test_*.py`、2472 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 138 個 `test_*.py`、2475 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ba350bef..0df7971a 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -178,3 +178,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2472 tests in 137 files. `ruff check` clean. - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-20 · 2026-09-25 · One AI submenu in the Dock menu · #fix #menu #jeditor + +- **What**: listing every menu entry of the started IDE showed the Dock menu with two submenus titled "AI" side by side: JEditor's (Chat UI) and one `extend_dock_menu` added for PyBreeze's five AI docks, which also replaced JEditor's `dock_ai_menu` attribute with its own. +- **Fix**: `extend_dock_menu` (`menu/tools/tools_menu.py`) adds the AI docks to JEditor's `dock_ai_menu` and builds an AI submenu of its own only when JEditor has none. `architecture.md` §6 lists `dock_menu` and `dock_ai_menu` among what PyBreeze relies on in `EditorMain`. +- **Tests**: `test_no_menu_has_two_entries_of_the_same_name` in `test_started_menus.py` walks every menu of the started IDE and fails on two entries of one name in the same menu; before the fix it found exactly `Dock > AI`. New `test_dock_menu.py` (2): the AI docks join an existing AI submenu, and get their own when there is none; the first failed before the fix. With `test_unsaved_on_close.py`, 15 pass. +- **Result / numbers**: 2475 tests in 138 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/menu/tools/tools_menu.py`, `test/test_utils/test_started_menus.py`, `test/test_utils/test_dock_menu.py`, `architecture.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7c279cdd..0dd610c3 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-20 | 2026-09-25 | One AI submenu in the Dock menu | #fix #menu #jeditor | [2026-09-c](2026-09-c.md) | | U-20260925-19 | 2026-09-25 | One space after the SSH terminal's [Error] | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-18 | 2026-09-25 | Regex and timestamp results worded in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-17 | 2026-09-25 | HTTP status classes in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | @@ -422,4 +423,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 19 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 20 | diff --git a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py index d7c30b9f..5e3a4211 100644 --- a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py +++ b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py @@ -253,9 +253,12 @@ def extend_dock_menu(ui_we_want_to_set: PyBreezeMainWindow): ui_we_want_to_set.dock_ssh_menu = ui_we_want_to_set.dock_menu.addMenu( language_wrapper.language_word_dict.get("extend_tools_menu_dock_ssh_menu") ) - ui_we_want_to_set.dock_ai_menu = ui_we_want_to_set.dock_menu.addMenu( - language_wrapper.language_word_dict.get("extend_tools_menu_dock_ai_menu") - ) + # JEditor's Dock menu has an AI submenu of its own (Chat UI): the review docks + # join it. A second one beside it showed two "AI" entries + if getattr(ui_we_want_to_set, "dock_ai_menu", None) is None: + ui_we_want_to_set.dock_ai_menu = ui_we_want_to_set.dock_menu.addMenu( + language_wrapper.language_word_dict.get("extend_tools_menu_dock_ai_menu") + ) for widget_key, attribute, menu_attribute, action_key in _DOCK_ACTIONS: _register_action( diff --git a/test/test_utils/test_dock_menu.py b/test/test_utils/test_dock_menu.py new file mode 100644 index 00000000..60116eda --- /dev/null +++ b/test/test_utils/test_dock_menu.py @@ -0,0 +1,49 @@ +"""PyBreeze's docks in JEditor's Dock menu: the AI ones join JEditor's AI submenu.""" +from __future__ import annotations + +import os +from types import SimpleNamespace + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtWidgets import QApplication, QMenu + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.menu.tools.tools_menu import extend_dock_menu + +_AI_DOCKS = 5 # AI Code-Review, CoT Prompt Editor, CoT Code Review, Skill Prompt Editor, Skill Send + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +def _submenus(menu: QMenu) -> list[str]: + return [action.text() for action in menu.actions() if action.menu() is not None] + + +def test_the_ai_docks_join_jeditors_ai_submenu(app): + dock_menu = QMenu() + jeditor_ai = dock_menu.addMenu("AI") + jeditor_ai.addAction("Chat UI") + window = SimpleNamespace(dock_menu=dock_menu, dock_ai_menu=jeditor_ai) + + extend_dock_menu(window) + + assert window.dock_ai_menu is jeditor_ai + assert _submenus(dock_menu).count("AI") == 1 + assert len(jeditor_ai.actions()) == 1 + _AI_DOCKS + + +def test_without_one_they_get_their_own(app): + dock_menu = QMenu() + window = SimpleNamespace(dock_menu=dock_menu) + + extend_dock_menu(window) + + assert "AI" in _submenus(dock_menu) + assert len(window.dock_ai_menu.actions()) == _AI_DOCKS diff --git a/test/test_utils/test_started_menus.py b/test/test_utils/test_started_menus.py index 5149a0e1..c657809a 100644 --- a/test/test_utils/test_started_menus.py +++ b/test/test_utils/test_started_menus.py @@ -73,3 +73,27 @@ def test_a_broken_extend_tab_costs_only_itself(tmp_path): assert "fine" in tabs assert "broken" not in tabs + + +_REPORT_REPEATED_ENTRIES = """ +def repeated(menu, path): + found = [] + texts = [action.text() for action in menu.actions() if not action.isSeparator()] + found.extend(" > ".join(path + [text]) for text in sorted(set(texts)) if texts.count(text) > 1) + for action in menu.actions(): + if action.menu() is not None: + found.extend(repeated(action.menu(), path + [action.text()])) + return found + +result = [ + entry + for top in window.menuBar().actions() if top.menu() is not None + for entry in repeated(top.menu(), [top.text()]) +] +""" + + +def test_no_menu_has_two_entries_of_the_same_name(tmp_path): + # Dock had two "AI" submenus side by side: JEditor's (Chat UI) and PyBreeze's + # (the review docks) + assert run_started_window(tmp_path, _REPORT_REPEATED_ENTRIES) == [] From 5360e8f376deb0f9fca7b8f5d641ec65495bf85b Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:01:55 +0800 Subject: [PATCH 072/166] Spell menu names one way and say Tab or Dock on every Tools entry --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 10 ++++ docs/updates/README.md | 3 +- images/menu_automation.png | Bin 10776 -> 10210 bytes .../extend_multi_language/extend_english.py | 49 +++++++++--------- .../extend_traditional_chinese.py | 39 +++++++------- pybreeze/pybreeze_ui/menu/tools/tools_menu.py | 2 +- test/test_utils/test_dock_menu.py | 2 +- test/test_utils/test_language_parity.py | 28 +++++++++- 9 files changed, 87 insertions(+), 48 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 18bf34f2..8df3eb00 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 138 個 `test_*.py`、2475 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 138 個 `test_*.py`、2478 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 0df7971a..8a91179e 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -187,3 +187,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2475 tests in 138 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/menu/tools/tools_menu.py`, `test/test_utils/test_started_menus.py`, `test/test_utils/test_dock_menu.py`, `architecture.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-21 · 2026-09-25 · Menu names spelled one way, and Tools entries that say Tab or Dock · #fix #menu #i18n #readme + +- **What**: listing every menu entry of the started IDE found names spelled two ways and entries that did not say what they open: the Automation menu wrote "Autocontrol" (Run Autocontrol Script, Open Autocontrol Doc, ...) where Install and the GUI entry wrote "AutoControl", as the README does; TestPioneer's entries read "Create TestPioneer Yaml template" and "Execute Test Pioneer Yaml" (Execute where every other menu says Run, and "Yaml"); Tools ▸ AI read "AI Code-Review Tab", "CoT Prompt Editor", "CoT Code Review Tab", "Skill Prompt Editor", "Skill Send GUI", where every other Tools entry ends in Tab; the Skill Send dock's entry read "Skill Prompt Dock" and its title "Skill Send GUI" (Traditional Chinese: Skill Prompt 傳送停駐窗格, Skill 提示詞傳送 GUI). +- **Fix**: AutoControl throughout, in both languages; "Create TestPioneer YAML Template" / "Run TestPioneer YAML" (建立 TestPioneer YAML 範本 / 執行 TestPioneer YAML) and "Please choose a YAML file"; "AI Code Review"; every Tools ▸ AI entry ends in Tab (分頁); Skill Send's tab and dock are "Skill Send" (Skill 提示詞傳送), its menu entries "Skill Send Tab" and "Skill Send Dock" (Skill 提示詞傳送分頁/停駐窗格). Skill Send's Tools entry had used its tab title as its label; it has a key of its own, `extend_tools_menu_skill_prompt_send_tab_action`. +- **Tests**: in `test_language_parity.py` (`TestLabelsAreTold`), `test_every_tools_entry_says_whether_it_opens_a_tab_or_a_dock` (every `_TAB_ACTIONS` entry ends in Tab / 分頁, every `_DOCK_ACTIONS` one in Dock / 停駐窗格), `test_a_dock_is_titled_like_the_tab_of_the_same_tool` and `test_a_name_is_spelled_one_way`; all three failed before the change. `test_no_label_is_in_capitals` counts YAML among the acronyms. With the menu and TestPioneer tests, 51 pass. +- **Result / numbers**: 2478 tests in 138 files. `ruff check` clean. +- **Docs**: `images/menu_automation.png` redone from the started IDE under dark_amber (AutoControl); `architecture_explore.md` (test count). +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `pybreeze/pybreeze_ui/menu/tools/tools_menu.py`, `test/test_utils/test_language_parity.py`, `test/test_utils/test_dock_menu.py`, `images/menu_automation.png`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0dd610c3..e89409d5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-21 | 2026-09-25 | Menu names spelled one way, and Tools entries that say Tab or Dock | #fix #menu #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-20 | 2026-09-25 | One AI submenu in the Dock menu | #fix #menu #jeditor | [2026-09-c](2026-09-c.md) | | U-20260925-19 | 2026-09-25 | One space after the SSH terminal's [Error] | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-18 | 2026-09-25 | Regex and timestamp results worded in the IDE's language | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | @@ -423,4 +424,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 20 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 21 | diff --git a/images/menu_automation.png b/images/menu_automation.png index 8bec583e243b79e56aa5419c10ba9175ef76c329..91edf86f439285a7cd2b19dc2959ed4d1ad0d470 100644 GIT binary patch literal 10210 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 E0h+7vM*si- literal 10776 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 diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index c11fe853..0c875897 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -40,17 +40,17 @@ "apitestka_github_label": "Open APITestka GitHub", "apitestka_github_tab_label": "APITestka GitHub", "apitestka_create_project_label": "Create APITestka Project", - # Autocontrol Menu - "autocontrol_menu_label": "Autocontrol", - "autocontrol_run_script_label": "Run Autocontrol Script", - "autocontrol_run_script_with_send_label": "Run Autocontrol With Send", - "autocontrol_run_multi_script_label": "Run Multi Autocontrol Script", - "autocontrol_run_multi_script_with_send_label": "Run Multi Autocontrol Script With Send", - "autocontrol_doc_label": "Open Autocontrol Doc", - "autocontrol_doc_tab_label": "Autocontrol Doc", - "autocontrol_github_label": "Open Autocontrol GitHub", - "autocontrol_github_tab_label": "Autocontrol GitHub", - "autocontrol_create_project_label": "Create Autocontrol Project", + # AutoControl Menu + "autocontrol_menu_label": "AutoControl", + "autocontrol_run_script_label": "Run AutoControl Script", + "autocontrol_run_script_with_send_label": "Run AutoControl With Send", + "autocontrol_run_multi_script_label": "Run Multi AutoControl Script", + "autocontrol_run_multi_script_with_send_label": "Run Multi AutoControl Script With Send", + "autocontrol_doc_label": "Open AutoControl Doc", + "autocontrol_doc_tab_label": "AutoControl Doc", + "autocontrol_github_label": "Open AutoControl GitHub", + "autocontrol_github_tab_label": "AutoControl GitHub", + "autocontrol_create_project_label": "Create AutoControl Project", "autocontrol_record_menu_label": "Record", "autocontrol_record_start_label": "Record Start", "autocontrol_record_stop_label": "Record Stop", @@ -112,12 +112,12 @@ "test_pioneer_label": "TestPioneer", "test_pioneer_github_label": "Open TestPioneer GitHub", "test_pioneer_github_tab_label": "TestPioneer GitHub", - "test_pioneer_create_template_label": "Create TestPioneer Yaml template", + "test_pioneer_create_template_label": "Create TestPioneer YAML Template", "test_pioneer_template_exists": "{path} already exists. Replace it with a fresh template and lose its content?", "test_pioneer_template_failed": "The template could not be created at {path}: {error}", "test_pioneer_template_created": "Template created: {path}", - "test_pioneer_run_yaml": "Execute Test Pioneer Yaml", - "test_pioneer_not_choose_yaml": "Please choose a Yaml file", + "test_pioneer_run_yaml": "Run TestPioneer YAML", + "test_pioneer_not_choose_yaml": "Please choose a YAML file", # prthinker code review "prthinker_menu_label": "Code Review (prthinker)", "prthinker_review_current_file_label": "Review the current file", @@ -283,7 +283,7 @@ "ssh_login_widget_dialog_title_choose_key": "Choose a private key", "ssh_login_widget_status_disconnected": "Disconnected", # AI Code Review GUI - "ai_code_review_gui_window_title": "AI Code-Review Client", + "ai_code_review_gui_window_title": "AI Code Review Client", "ai_code_review_gui_label_url": "URL:", "ai_code_review_gui_label_method": "Method:", "ai_code_review_gui_label_code_to_send": "Code to Send:", @@ -334,29 +334,30 @@ "extend_tools_menu_tools_ai_menu": "AI", "extend_tools_menu_ssh_client_tab_action": "SSH Client Tab", "extend_tools_menu_ssh_client_tab_label": "SSH Client", - "extend_tools_menu_ai_code_review_tab_action": "AI Code-Review Tab", - "extend_tools_menu_ai_code_review_tab_label": "AI Code-Review", - "extend_tools_menu_cot_prompt_editor_tab_action": _COT_PROMPT_EDITOR, + "extend_tools_menu_ai_code_review_tab_action": "AI Code Review Tab", + "extend_tools_menu_ai_code_review_tab_label": "AI Code Review", + "extend_tools_menu_cot_prompt_editor_tab_action": _COT_PROMPT_EDITOR + " Tab", "extend_tools_menu_cot_prompt_editor_tab_label": _COT_PROMPT_EDITOR, "extend_tools_menu_cot_code_review_tab_action": "CoT Code Review Tab", "extend_tools_menu_cot_code_review_tab_label": "CoT Code Review", - "extend_tools_menu_skill_prompt_editor_tab_action": _SKILL_PROMPT_EDITOR, + "extend_tools_menu_skill_prompt_editor_tab_action": _SKILL_PROMPT_EDITOR + " Tab", "extend_tools_menu_skill_prompt_editor_tab_label": _SKILL_PROMPT_EDITOR, - "extend_tools_menu_skill_prompt_send_tab_label": "Skill Send GUI", + "extend_tools_menu_skill_prompt_send_tab_label": "Skill Send", + "extend_tools_menu_skill_prompt_send_tab_action": "Skill Send Tab", "extend_tools_menu_dock_ssh_menu": "SSH", "extend_tools_menu_dock_ai_menu": "AI", "extend_tools_menu_ssh_client_dock_action": "SSH Client Dock", - "extend_tools_menu_ai_code_review_dock_action": "AI Code-Review Dock", + "extend_tools_menu_ai_code_review_dock_action": "AI Code Review Dock", "extend_tools_menu_cot_prompt_editor_dock_action": "CoT Prompt Editor Dock", "extend_tools_menu_cot_code_review_dock_action": "CoT Code Review Dock", "extend_tools_menu_cot_code_review_dock_title": "CoT Code Review", "extend_tools_menu_skill_prompt_editor_dock_action": "Skill Prompt Editor Dock", "extend_tools_menu_ssh_client_dock_title": "SSH Client", - "extend_tools_menu_ai_code_review_dock_title": "AI Code-Review", + "extend_tools_menu_ai_code_review_dock_title": "AI Code Review", "extend_tools_menu_cot_prompt_editor_dock_title": "CoT Prompt Editor", "extend_tools_menu_skill_prompt_editor_dock_title": "Skill Prompt Editor", - "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt Dock", - "extend_tools_menu_skill_prompt_send_dock_title": "Skill Send GUI", + "extend_tools_menu_skill_prompt_send_dock_action": "Skill Send Dock", + "extend_tools_menu_skill_prompt_send_dock_title": "Skill Send", # CoT code-review GUI "cot_gui_window_title": "Prompt Sender UI", "cot_gui_label_api_url": "API URL:", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 7a3ce491..ebe0e33e 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -35,17 +35,17 @@ "apitestka_github_label": "開啟 APITestka GitHub", "apitestka_github_tab_label": "APITestka GitHub", "apitestka_create_project_label": "建立 APITestka 專案", - # Autocontrol Menu - "autocontrol_menu_label": "Autocontrol", - "autocontrol_run_script_label": "執行 Autocontrol 腳本", - "autocontrol_run_script_with_send_label": "執行 Autocontrol 腳本並寄信", - "autocontrol_run_multi_script_label": "執行多個 Autocontrol 腳本", - "autocontrol_run_multi_script_with_send_label": "執行多個 Autocontrol 腳本並寄信", - "autocontrol_doc_label": "開啟 Autocontrol 文件", - "autocontrol_doc_tab_label": "Autocontrol 文件", - "autocontrol_github_label": "開啟 Autocontrol GitHub", - "autocontrol_github_tab_label": "Autocontrol GitHub", - "autocontrol_create_project_label": "建立 Autocontrol 專案", + # AutoControl Menu + "autocontrol_menu_label": "AutoControl", + "autocontrol_run_script_label": "執行 AutoControl 腳本", + "autocontrol_run_script_with_send_label": "執行 AutoControl 腳本並寄信", + "autocontrol_run_multi_script_label": "執行多個 AutoControl 腳本", + "autocontrol_run_multi_script_with_send_label": "執行多個 AutoControl 腳本並寄信", + "autocontrol_doc_label": "開啟 AutoControl 文件", + "autocontrol_doc_tab_label": "AutoControl 文件", + "autocontrol_github_label": "開啟 AutoControl GitHub", + "autocontrol_github_tab_label": "AutoControl GitHub", + "autocontrol_create_project_label": "建立 AutoControl 專案", "autocontrol_record_menu_label": "錄製", "autocontrol_record_start_label": "開始錄製", "autocontrol_record_stop_label": "停止錄製", @@ -107,12 +107,12 @@ "test_pioneer_label": "TestPioneer", "test_pioneer_github_label": "開啟 TestPioneer GitHub", "test_pioneer_github_tab_label": "TestPioneer GitHub", - "test_pioneer_create_template_label": "建立 TestPioneer Yaml 範本", + "test_pioneer_create_template_label": "建立 TestPioneer YAML 範本", "test_pioneer_template_exists": "{path} 已經存在。要用新的範本取代,並捨棄它目前的內容嗎?", "test_pioneer_template_failed": "無法在 {path} 建立範本:{error}", "test_pioneer_template_created": "已建立範本:{path}", - "test_pioneer_run_yaml": "執行 Test Pioneer Yaml", - "test_pioneer_not_choose_yaml": "請選擇 Yaml 檔案", + "test_pioneer_run_yaml": "執行 TestPioneer YAML", + "test_pioneer_not_choose_yaml": "請選擇 YAML 檔案", # prthinker code review "prthinker_menu_label": "程式碼審查(prthinker)", "prthinker_review_current_file_label": "審查目前的檔案", @@ -326,13 +326,14 @@ "extend_tools_menu_ssh_client_tab_label": "SSH 用戶端", "extend_tools_menu_ai_code_review_tab_action": "AI 程式碼審查分頁", "extend_tools_menu_ai_code_review_tab_label": "AI 程式碼審查", - "extend_tools_menu_cot_prompt_editor_tab_action": _COT_PROMPT_EDITOR, + "extend_tools_menu_cot_prompt_editor_tab_action": _COT_PROMPT_EDITOR + "分頁", "extend_tools_menu_cot_prompt_editor_tab_label": _COT_PROMPT_EDITOR, "extend_tools_menu_cot_code_review_tab_action": "CoT 程式碼審查分頁", "extend_tools_menu_cot_code_review_tab_label": "CoT 程式碼審查", - "extend_tools_menu_skill_prompt_editor_tab_action": _SKILL_PROMPT_EDITOR, + "extend_tools_menu_skill_prompt_editor_tab_action": _SKILL_PROMPT_EDITOR + "分頁", "extend_tools_menu_skill_prompt_editor_tab_label": _SKILL_PROMPT_EDITOR, - "extend_tools_menu_skill_prompt_send_tab_label": "Skill 提示詞傳送 GUI", + "extend_tools_menu_skill_prompt_send_tab_label": "Skill 提示詞傳送", + "extend_tools_menu_skill_prompt_send_tab_action": "Skill 提示詞傳送分頁", "extend_tools_menu_dock_ssh_menu": "SSH", "extend_tools_menu_dock_ai_menu": "AI", "extend_tools_menu_ssh_client_dock_action": "SSH 用戶端停駐窗格", @@ -345,8 +346,8 @@ "extend_tools_menu_ai_code_review_dock_title": "AI 程式碼審查", "extend_tools_menu_cot_prompt_editor_dock_title": _COT_PROMPT_EDITOR, "extend_tools_menu_skill_prompt_editor_dock_title": _SKILL_PROMPT_EDITOR, - "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt 傳送停駐窗格", - "extend_tools_menu_skill_prompt_send_dock_title": "Skill 提示詞傳送 GUI", + "extend_tools_menu_skill_prompt_send_dock_action": "Skill 提示詞傳送停駐窗格", + "extend_tools_menu_skill_prompt_send_dock_title": "Skill 提示詞傳送", # CoT code-review GUI "cot_gui_window_title": "Prompt 傳送介面", "cot_gui_label_api_url": "API URL:", diff --git a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py index 5e3a4211..a8b820ec 100644 --- a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py +++ b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py @@ -107,7 +107,7 @@ "extend_tools_menu_skill_prompt_editor_tab_action", "extend_tools_menu_skill_prompt_editor_tab_label"), ("SkillSendGUI", "tools_ai_skill_send_action", "tools_ai_menu", - "extend_tools_menu_skill_prompt_send_tab_label", + "extend_tools_menu_skill_prompt_send_tab_action", "extend_tools_menu_skill_prompt_send_tab_label"), ("DiagramEditor", "tools_diagram_editor_action", "tools_menu", "extend_tools_menu_diagram_editor_tab_action", "extend_tools_menu_diagram_editor_tab_label"), diff --git a/test/test_utils/test_dock_menu.py b/test/test_utils/test_dock_menu.py index 60116eda..bd8b502f 100644 --- a/test/test_utils/test_dock_menu.py +++ b/test/test_utils/test_dock_menu.py @@ -12,7 +12,7 @@ from pybreeze.extend_multi_language.update_language_dict import update_language_dict from pybreeze.pybreeze_ui.menu.tools.tools_menu import extend_dock_menu -_AI_DOCKS = 5 # AI Code-Review, CoT Prompt Editor, CoT Code Review, Skill Prompt Editor, Skill Send +_AI_DOCKS = 5 # AI Code Review, CoT Prompt Editor, CoT Code Review, Skill Prompt Editor, Skill Send @pytest.fixture(scope="module") diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index dd06d3ec..c1dff29e 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -116,6 +116,32 @@ def test_a_dock_is_titled_like_its_tab(self): assert EN[dock] == EN[tab] assert ZH[dock] == ZH[tab] + def test_every_tools_entry_says_whether_it_opens_a_tab_or_a_dock(self): + # Tools > AI read "CoT Prompt Editor", "Skill Send GUI" beside "CoT Code Review Tab" + from pybreeze.pybreeze_ui.menu.tools.tools_menu import _DOCK_ACTIONS, _TAB_ACTIONS + + for *_start, action_key, _label_key in _TAB_ACTIONS: + assert EN[action_key].endswith(" Tab") and ZH[action_key].endswith("分頁"), action_key + for *_start, action_key in _DOCK_ACTIONS: + assert EN[action_key].endswith(" Dock") and ZH[action_key].endswith("停駐窗格"), action_key + + def test_a_dock_is_titled_like_the_tab_of_the_same_tool(self): + # "AI Code-Review" and "Skill Send GUI" as titles; the Skill Send dock's + # menu entry read "Skill Prompt Dock" + for tool in ("ai_code_review", "cot_code_review", "cot_prompt_editor", "skill_prompt_editor", + "skill_prompt_send"): + for words in (EN, ZH): + tab = words[f"extend_tools_menu_{tool}_tab_label"] + assert words[f"extend_tools_menu_{tool}_dock_title"] == tab + assert words[f"extend_tools_menu_{tool}_dock_action"].startswith(tab), tool + + def test_a_name_is_spelled_one_way(self): + # "Autocontrol" beside "AutoControl GUI" and "Install AutoControl"; + # "Test Pioneer" beside "TestPioneer"; "Yaml" for YAML + misspelt = {key: value for words in (EN, ZH) for key, value in words.items() + if re.search(r"Autocontrol|Test Pioneer|Yaml|Code-Review", str(value))} + assert misspelt == {} + def test_the_same_action_reads_the_same_on_every_tab(self): # Header Analyzer said "Open token in JWT decoder", Response Inspector # "Open JWT in decoder", for the same hand-over @@ -162,7 +188,7 @@ def getaddrinfo(host, *_args, **_kwargs): def test_no_label_is_in_capitals(self): # Every automation menu's Help submenu read "HELP", beside JEditor's "Help" menu - acronyms = {"HTTP", "JSON", "MIME", "SFTP"} + acronyms = {"HTTP", "JSON", "MIME", "SFTP", "YAML"} shouted = {key: text for key, text in EN.items() if key.endswith("_label") for word in re.findall(r"\b[A-Z]{4,}\b", text) if word not in acronyms} assert shouted == {} From d7d5d51c887966d5e4e6476d2259a7253749d2a1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:04:29 +0800 Subject: [PATCH 073/166] Name the FileAutomation install entry as the menu does --- docs/updates/2026-09-c.md | 10 ++++++++++ docs/updates/README.md | 3 ++- images/menu_install.png | Bin 7863 -> 7864 bytes progress.md | 2 +- .../extend_multi_language/extend_english.py | 2 +- .../extend_traditional_chinese.py | 2 +- test/test_utils/test_language_parity.py | 5 +++-- 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 8a91179e..8d530287 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -197,3 +197,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: `images/menu_automation.png` redone from the started IDE under dark_amber (AutoControl); `architecture_explore.md` (test count). - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `pybreeze/pybreeze_ui/menu/tools/tools_menu.py`, `test/test_utils/test_language_parity.py`, `test/test_utils/test_dock_menu.py`, `images/menu_automation.png`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-22 · 2026-09-25 · Install FileAutomation, as the menu and the README call it · #fix #menu #i18n + +- **What**: the menu dump of the started IDE in Traditional Chinese (after U-20260925-21's English one) showed Install ▸ Automation offering "Install Automation File" (安裝 Automation File) for the package the Automation menu and the README call FileAutomation. It also showed JEditor's Dock menu named 區域 (新編輯器區域, 新瀏覽器區域) while PyBreeze's entries in it say 停駐窗格, and JEditor's Editor, Git and Tools submenus untranslated: JEditor's own strings. +- **Fix**: the entry reads "Install FileAutomation" (安裝 FileAutomation). The JEditor wording joins `progress.md` #108 (JEditor's words, with line numbers at bb6bc94). +- **Tests**: `test_a_name_is_spelled_one_way` also refuses "Automation File"; it failed on this key before the fix. With the install menu tests, 30 pass. +- **Result / numbers**: 2478 tests in 138 files. `ruff check` clean. +- **Docs**: `images/menu_install.png` redone at its 220×356 under dark_amber; `progress.md` #108. +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `images/menu_install.png`, `progress.md` +- **Open items**: `progress.md` #108. diff --git a/docs/updates/README.md b/docs/updates/README.md index e89409d5..a1f05a46 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-22 | 2026-09-25 | Install FileAutomation, as the menu and the README call it | #fix #menu #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-21 | 2026-09-25 | Menu names spelled one way, and Tools entries that say Tab or Dock | #fix #menu #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-20 | 2026-09-25 | One AI submenu in the Dock menu | #fix #menu #jeditor | [2026-09-c](2026-09-c.md) | | U-20260925-19 | 2026-09-25 | One space after the SSH terminal's [Error] | #fix #ssh #i18n | [2026-09-c](2026-09-c.md) | @@ -424,4 +425,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 21 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 22 | diff --git a/images/menu_install.png b/images/menu_install.png index 5cd5650ed0d0572241cd75957a8abd81de09ef13..3db56352911878b0493d92539e281f7acd2d2555 100644 GIT binary patch delta 6642 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 Y{cW^bY-R)S{Q+JYs=5!qC_jJuU)3F{^Z)<= diff --git a/progress.md b/progress.md index 82a0c849..9b601813 100644 --- a/progress.md +++ b/progress.md @@ -16,7 +16,7 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#102** [BLOCKED] A report mail can wait forever: je_mail_thunder's `SMTPWrapper(host, port)` passes no timeout to `SMTP_SSL`, so a mail server that accepts the connection and then stalls holds the report-mail thread (`extend/mail_thunder_extend/mail_thunder_setting.py`, `send_report`) with no end, and the run window never says whether the report went. Needs a `timeout` argument in MailThunder (`je_mail_thunder/smtp/smtp_wrapper.py:20`); then pass 30 s here. - **#103** [BLOCKED] PyBreeze's automation keywords are never highlighted: `syntax_extend_package` registers them for `.json`, `.yml` and `.yaml` (`pybreeze_ui/syntax/syntax_extend.py`), but JEditor has built-in rules for those suffixes, so `CodeEditor.reset_highlighter` gets a `GenericHighlighter` from `highlighter_for`, and only `PythonHighlighter` reads the plugin registry (`je_editor/pyside_ui/code/syntax/generic_syntax.py:124`, `code_edit_plaintext.py:423-425` in `D:\Codes\JEDITOR` at d372c35). Needs JEditor's generic highlighter to add a registered language's words for the suffix, taking the colour as a theme colour key as its own rules do (PyBreeze registers `warning_output_color` and `diff_modified_marker_color`); the `architecture.md` §6 contract changes with it. - **#106** [DECIDE] `test_pioneer` is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), and before 0.1.34 it read a YAML file in the locale's encoding: on a Traditional Chinese Windows (cp950) TestPioneer "Run YAML" of a file with a non-ASCII character, which the editor saves as UTF-8, exits 1 with `UnicodeDecodeError` (`extend/process_executor/test_pioneer/test_pioneer_process_manager.py`). This repo's `.venv` has 0.1.30; 0.1.34 reads it as UTF-8. `Install ▸ Automation ▸ Install TestPioneer` upgrades it by hand meanwhile. Should the dependencies require `test_pioneer>=0.1.34`? -- **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. +- **#108** [BLOCKED] The Traditional Chinese IDE still shows Mainland terms in JEditor's own entries, which PyBreeze does not define: the Run menu (`run_menu_label` 運行, `run_menu_run_program_label` 運行程式, `run_menu_run_on_shell_label` 在終端運行, ...), the font menus (`file_menu_font_label` / `text_menu_label_font` 字體, `font_size` 字體大小) and `editor_code_result` 程式運行結果 (`je_editor/utils/multi_language/traditional_chinese.py:54`, `:96-97` and on, in `D:\Codes\JEDITOR` at e1a8b12). PyBreeze's own entries use 執行, 字型, 外掛 and 終端機 (`test_traditional_chinese_uses_taiwan_terms`), so its menus now sit beside JEditor's 運行. The Dock menu is 區域 (`dock_menu_label`, with 新編輯器區域, 新瀏覽器區域, `:64-68` at bb6bc94) while PyBreeze's entries in it say 停駐窗格, and its Editor, Git and Tools submenus (`:78-80`) are not translated. Needs the words changed in JEditor; overriding them here would copy JEditor's English entries into both of PyBreeze's dictionaries. - **#109** [DECIDE] `je-editor>=1.0.27` (`requirements.txt`, `pyproject.toml`, `dev.toml`) lets an environment keep a gitpython below 3.1.59, which has CVE-2026-78676 (a config write turns a quoted value into a live `core.hooksPath`: code runs on the next hook) and CVE-2026-78679; the IDE's git features use it (JEditor's `je_editor/git_client/`). je-editor 1.0.28 differs from 1.0.27 only in requiring `gitpython>=3.1.59`, and 3.1.59 still has CVE-2026-87818 (the diff API's `--no-index` reads any path), fixed in 3.1.60. Should the dependencies say `je-editor>=1.0.28`, declare `gitpython>=3.1.60`, or both? JEditor's own floor is its to raise (`D:\Codes\JEDITOR\pyproject.toml:18`). - **#110** [BLOCKED] A file opened from the project tree shows as unsaved (" *" on its tab, `_is_modified` true) before anything is typed: JEditor's `EditorWidget.open_an_file` fills the editor with `setPlainText`, which fires `_on_text_changed`, and never clears the mark afterwards (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:295` and `:347-358` in `D:\Codes\JEDITOR` at 1bf81e3; plain `EditorMain` does the same). Needs `open_an_file` to clear the mark (`mark_saved()`) once the file is loaded. - **#111** [DECIDE] AI Code Review, CoT Code Review and Skill Send refuse an endpoint on this machine or on a private network (`validate_url` in `ai_code_review_gui.py:77`, `code_review_thread.py:33`, `skills_send_gui.py:55`; `url_validation._is_blocked_ip`), so a local model server (Ollama, LM Studio, a prthinker server on localhost) cannot be used from them, although CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` until U-20260925-11. The Security › Network rule rejects loopback and private addresses for every user-supplied URL. Should these panels let the user allow a loopback (or private) endpoint, for example after a confirmation naming the address, or stay public-only? diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 0c875897..fd5cc8b3 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -102,7 +102,7 @@ "install_menu_apitestka": "Install APITestka", "install_menu_loaddensity": "Install LoadDensity", "install_menu_webrunner": "Install WebRunner", - "install_menu_automation_file": "Install Automation File", + "install_menu_automation_file": "Install FileAutomation", "install_menu_mail_thunder": "Install MailThunder", "install_menu_test_pioneer": "Install TestPioneer", "install_menu_prthinker": "Install prthinker (code review)", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index ebe0e33e..f95057ae 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -97,7 +97,7 @@ "install_menu_apitestka": "安裝 APITestka", "install_menu_loaddensity": "安裝 LoadDensity", "install_menu_webrunner": "安裝 WebRunner", - "install_menu_automation_file": "安裝 Automation File", + "install_menu_automation_file": "安裝 FileAutomation", "install_menu_mail_thunder": "安裝 MailThunder", "install_menu_test_pioneer": "安裝 TestPioneer", "install_menu_prthinker": "安裝 prthinker(程式碼審查)", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index c1dff29e..9799407b 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -137,9 +137,10 @@ def test_a_dock_is_titled_like_the_tab_of_the_same_tool(self): def test_a_name_is_spelled_one_way(self): # "Autocontrol" beside "AutoControl GUI" and "Install AutoControl"; - # "Test Pioneer" beside "TestPioneer"; "Yaml" for YAML + # "Test Pioneer" beside "TestPioneer"; "Yaml" for YAML; "Install Automation File" + # beside the FileAutomation menu misspelt = {key: value for words in (EN, ZH) for key, value in words.items() - if re.search(r"Autocontrol|Test Pioneer|Yaml|Code-Review", str(value))} + if re.search(r"Autocontrol|Test Pioneer|Yaml|Code-Review|Automation File", str(value))} assert misspelt == {} def test_the_same_action_reads_the_same_on_every_tab(self): From d9d3c1e2d04b25d7932fbd591b126a703a06189c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:06:38 +0800 Subject: [PATCH 074/166] Default the delete and discard questions to No --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 ++++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_file_viewer_widget.py | 3 +- .../diagram_editor/diagram_editor_widget.py | 1 + .../editor_main/file_tree_context_menu.py | 1 + .../test_questions_default_to_no.py | 41 +++++++++++++++++++ 7 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 test/test_utils/test_questions_default_to_no.py diff --git a/architecture_explore.md b/architecture_explore.md index 8df3eb00..03c630e0 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 138 個 `test_*.py`、2478 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 139 個 `test_*.py`、2480 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 8d530287..363a2e60 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -207,3 +207,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: `images/menu_install.png` redone at its 220×356 under dark_amber; `progress.md` #108. - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `images/menu_install.png`, `progress.md` - **Open items**: `progress.md` #108. + +## U-20260925-23 · 2026-09-25 · Deleting and discarding ask with No as the default · #fix #ui #ssh #diagram + +- **What**: three of the nine yes-or-no questions named no default button: deleting a file or folder on the SFTP server, deleting one in the project tree, and discarding the diagram for a new one. Given Yes and No and no default, Qt makes Yes the default, so Enter, or a key press meant for the window behind, deleted the file for good (no recycle bin on either side) or dropped the diagram. The other six already defaulted to No. +- **Fix**: `SSHFileTreeManager.action_delete`, the project tree's `_action_delete` and `DiagramEditorWidget._new_diagram` pass `QMessageBox.StandardButton.No` as the default. +- **Tests**: new `test_questions_default_to_no.py` (2) reads every `QMessageBox.question` call in the package and fails on one without a default button (positional or `defaultButton=`), and checks it sees them all. It named exactly the three before the fix. The file tree, SFTP tree, diagram editor and message box tests pass (102). +- **Result / numbers**: 2480 tests in 139 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `test/test_utils/test_questions_default_to_no.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a1f05a46..e756e44b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-23 | 2026-09-25 | Deleting and discarding ask with No as the default | #fix #ui #ssh #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-22 | 2026-09-25 | Install FileAutomation, as the menu and the README call it | #fix #menu #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-21 | 2026-09-25 | Menu names spelled one way, and Tools entries that say Tab or Dock | #fix #menu #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-20 | 2026-09-25 | One AI submenu in the Dock menu | #fix #menu #jeditor | [2026-09-c](2026-09-c.md) | @@ -425,4 +426,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 22 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 23 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index 19af2dd5..8dc6e162 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -554,7 +554,8 @@ def action_delete(self, item: QTreeWidgetItem | None): self, self.word_dict.get("ssh_file_viewer_dialog_title_confirm_delete"), as_text(f"{self.word_dict.get('ssh_file_viewer_dialog_message_confirm_delete')} '{path}'?"), - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) if reply != QMessageBox.StandardButton.Yes: return diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py index 6076dcc0..70ca11a2 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py @@ -399,6 +399,7 @@ def _new_diagram(self) -> None: _lang("diagram_editor_confirm_title", "Confirm"), _lang("diagram_editor_confirm_new", "Discard current diagram?"), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) if reply != QMessageBox.StandardButton.Yes: return diff --git a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py index 784e4800..1f85adff 100644 --- a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py +++ b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py @@ -355,6 +355,7 @@ def _action_delete(tree_view: QTreeView, main_window, path: Path | None) -> None word.get("file_tree_ctx_confirm_delete"), as_text(word.get("file_tree_ctx_confirm_delete_message").format(name=str(path))), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, ) if reply != QMessageBox.StandardButton.Yes: return diff --git a/test/test_utils/test_questions_default_to_no.py b/test/test_utils/test_questions_default_to_no.py new file mode 100644 index 00000000..962925d1 --- /dev/null +++ b/test/test_utils/test_questions_default_to_no.py @@ -0,0 +1,41 @@ +"""A yes-or-no question names its default button, so Enter does not answer Yes by itself. + +Given Yes and No and no default, Qt makes Yes the default: Enter, or a key press +meant for the window behind, deleted a file on the SFTP server or in the project, +or dropped the diagram being drawn. +""" +from __future__ import annotations + +import ast +from pathlib import Path + +import pybreeze + +# QMessageBox.question(parent, title, text, buttons, defaultButton) +_DEFAULT_BUTTON_POSITION = 4 + + +def _questions_without_a_default() -> list[str]: + found = [] + for path in Path(pybreeze.__file__).parent.rglob("*.py"): + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and node.func.attr == "question" and ast.unparse(node.func.value) == "QMessageBox"): + continue + named = any(keyword.arg == "defaultButton" for keyword in node.keywords) + if not named and len(node.args) <= _DEFAULT_BUTTON_POSITION: + found.append(f"{path.name}:{node.lineno}") + return found + + +def test_every_question_names_its_default_button(): + assert _questions_without_a_default() == [] + + +def test_the_check_sees_the_questions(): + # Nine questions ask in the package; a check that saw none would pass on anything + count = sum( + 1 for path in Path(pybreeze.__file__).parent.rglob("*.py") + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "question") + assert count >= 9 From bf040e4b66b0bbde38a80ecd8563ceab66da5841 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:09:11 +0800 Subject: [PATCH 075/166] Make the diagram image suffix allowlist public --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py | 10 +++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 363a2e60..b29efeeb 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -216,3 +216,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2480 tests in 139 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `test/test_utils/test_questions_default_to_no.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-24 · 2026-09-25 · Refactor: the diagram image suffix allowlist public, its comment back on it · #refactor #diagram + +- **What**: the allowlist of image suffixes a saved diagram may reference on disk (`diagram_scene._VALID_IMAGE_SUFFIXES`) is needed by the editor's Add Image dialog too (U-20260925-25), and its comment, which says why it is a security boundary, sat above `_PASTE_OFFSET` instead, split from it when that constant was added. +- **Change**: renamed `IMAGE_SUFFIXES`; the comment is back above it. No behaviour changes. +- **Tests**: the diagram image and serialization tests pass (73). +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index e756e44b..6a202611 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-24 | 2026-09-25 | Refactor: the diagram image suffix allowlist public, its comment back on it | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-23 | 2026-09-25 | Deleting and discarding ask with No as the default | #fix #ui #ssh #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-22 | 2026-09-25 | Install FileAutomation, as the menu and the README call it | #fix #menu #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-21 | 2026-09-25 | Menu names spelled one way, and Tools entries that say Tab or Dock | #fix #menu #i18n #readme | [2026-09-c](2026-09-c.md) | @@ -426,4 +427,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 23 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 24 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py index 63cb67b9..9eb8c2d2 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py @@ -31,13 +31,13 @@ ) from pybreeze.utils.logging.logger import pybreeze_logger -# Allowlist of image extensions that a saved diagram may reference on disk. -# Defined once at module scope because it is a security boundary (only these -# local files are read back when reloading a ``.diagram.json``). # How far a pasted copy sits from the original, so it is visible as a copy _PASTE_OFFSET = 30 -_VALID_IMAGE_SUFFIXES = frozenset( +# Allowlist of image extensions that a saved diagram may reference on disk. +# Defined once at module scope because it is a security boundary (only these +# local files are read back when reloading a ``.diagram.json``). +IMAGE_SUFFIXES = frozenset( {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".svg", ".webp", ".ico"} ) @@ -837,7 +837,7 @@ def _try_load_image_source(self, img: DiagramImage, source: str) -> None: # to that host, so a diagram from someone else could collect the user's # credentials, and an unreachable host would block the UI thread until # SMB gives up. - if (path.suffix.lower() in _VALID_IMAGE_SUFFIXES + if (path.suffix.lower() in IMAGE_SUFFIXES and _is_on_this_machine(source) and path.is_file()): pix = QPixmap(str(path)) if not pix.isNull(): From fa570a645fa1a72a4eebcdfc5c9fc284da7d57ae Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:11:51 +0800 Subject: [PATCH 076/166] Translate the diagram editor's file filters and offer every image suffix --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 ++++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 5 +++ .../extend_traditional_chinese.py | 5 +++ .../diagram_editor/diagram_editor_widget.py | 25 ++++++++--- test/test_utils/test_diagram_editor_widget.py | 44 +++++++++++++++++++ test/test_utils/test_language_parity.py | 5 ++- 8 files changed, 88 insertions(+), 10 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 03c630e0..d28e7a3f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 139 個 `test_*.py`、2480 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 139 個 `test_*.py`、2482 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index b29efeeb..d51e212f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -224,3 +224,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Tests**: the diagram image and serialization tests pass (73). - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_scene.py` - **Open items**: none. + +## U-20260925-25 · 2026-09-25 · The diagram editor's file dialogs in the IDE's language, offering every image it keeps · #fix #diagram #i18n + +- **What**: the diagram editor's five file dialogs (Open, Save As, Export PNG, Export SVG, Add Image from file) passed English filter literals, "Diagram JSON (*.diagram.json);;All Files (*)", "PNG Image (*.png)" and so on, in every IDE language; the other tools' dialogs take theirs from the dictionary. Add Image's filter listed seven suffixes, one fewer than the eight a saved diagram may reference (`.ico` was missing), so an `.ico` could be picked only through All Files. +- **Fix**: the filters come from `diagram_editor_filter_diagram`, `_png`, `_svg`, `_images` and `_all` (架構圖 JSON, PNG 圖片, SVG 圖片, 圖片, 所有檔案, with the patterns in ASCII parentheses, which Qt reads); `_image_filter()` builds Add Image's from `diagram_scene.IMAGE_SUFFIXES` (U-20260925-24), so the two cannot drift apart. +- **Tests**: new `TestFileDialogFilters` in `test_diagram_editor_widget.py` (2): in the Traditional Chinese IDE no filter holds English words and Open and Save offer 所有檔案; the image filter holds every suffix of `IMAGE_SUFFIXES`. Both failed before the fix. `test_traditional_chinese_uses_full_width_punctuation` lets a filter's `({patterns})` keep its ASCII parentheses, as it did `(*.txt)`, and still names the twelve strings U-20260925-02 fixed when run on the old dictionary. With the diagram image and language tests, 56 pass. +- **Result / numbers**: 2482 tests in 139 files. `ruff check` clean. The full suite at `d7d5d51` passed (2463 passed, 15 skipped). +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_diagram_editor_widget.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6a202611..80ccea8c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-25 | 2026-09-25 | The diagram editor's file dialogs in the IDE's language, offering every image it keeps | #fix #diagram #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-24 | 2026-09-25 | Refactor: the diagram image suffix allowlist public, its comment back on it | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-23 | 2026-09-25 | Deleting and discarding ask with No as the default | #fix #ui #ssh #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-22 | 2026-09-25 | Install FileAutomation, as the menu and the README call it | #fix #menu #i18n | [2026-09-c](2026-09-c.md) | @@ -427,4 +428,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 24 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 25 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index fd5cc8b3..bcba7305 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -713,6 +713,11 @@ "diagram_editor_confirm_title": "Confirm", "diagram_editor_confirm_new": "Discard current diagram?", "diagram_editor_dialog_open": "Open Diagram", + "diagram_editor_filter_diagram": "Diagram JSON (*.diagram.json)", + "diagram_editor_filter_png": "PNG Image (*.png)", + "diagram_editor_filter_svg": "SVG Image (*.svg)", + "diagram_editor_filter_images": "Images ({patterns})", + "diagram_editor_filter_all": "All Files (*)", "diagram_editor_dialog_save": "Save Diagram", "diagram_editor_dialog_export_png": "Export PNG", "diagram_editor_dialog_export_svg": "Export SVG", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index f95057ae..1f051457 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -677,6 +677,11 @@ "diagram_editor_confirm_title": "確認", "diagram_editor_confirm_new": "是否捨棄目前的架構圖?", "diagram_editor_dialog_open": "開啟架構圖", + "diagram_editor_filter_diagram": "架構圖 JSON (*.diagram.json)", + "diagram_editor_filter_png": "PNG 圖片 (*.png)", + "diagram_editor_filter_svg": "SVG 圖片 (*.svg)", + "diagram_editor_filter_images": "圖片 ({patterns})", + "diagram_editor_filter_all": "所有檔案 (*)", "diagram_editor_dialog_save": "儲存架構圖", "diagram_editor_dialog_export_png": "匯出 PNG", "diagram_editor_dialog_export_svg": "匯出 SVG", diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py index 70ca11a2..6b7ca386 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py @@ -27,7 +27,9 @@ from pybreeze.pybreeze_ui.diagram_editor.diagram_mermaid_parser import parse_mermaid from pybreeze.pybreeze_ui.diagram_editor.diagram_property_panel import DiagramPropertyPanel -from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene, ImageDownloadThread, ToolMode +from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import ( + IMAGE_SUFFIXES, DiagramScene, ImageDownloadThread, ToolMode, +) from pybreeze.pybreeze_ui.diagram_editor.diagram_view import DiagramView from pybreeze.pybreeze_ui.error_text import error_text from pybreeze.pybreeze_ui.fixed_pitch import use_fixed_pitch_font @@ -43,6 +45,17 @@ def _lang(key: str, fallback: str = "") -> str: return language_wrapper.language_word_dict.get(key, fallback or key) +def _diagram_filter() -> str: + """The Open and Save dialogs' filter: diagrams, then any file.""" + return f"{_lang('diagram_editor_filter_diagram')};;{_lang('diagram_editor_filter_all')}" + + +def _image_filter() -> str: + """Add Image's filter: every suffix a saved diagram may keep (``IMAGE_SUFFIXES``), then any file.""" + patterns = " ".join(f"*{suffix}" for suffix in sorted(IMAGE_SUFFIXES)) + return f"{_lang('diagram_editor_filter_images').format(patterns=patterns)};;{_lang('diagram_editor_filter_all')}" + + _STATUS_HINTS: dict[ToolMode, str] = { ToolMode.SELECT: "diagram_editor_status_select", ToolMode.ADD_RECT: "diagram_editor_status_add_node", @@ -413,7 +426,7 @@ def _open_diagram(self) -> None: self, _lang("diagram_editor_dialog_open", "Open Diagram"), "", - "Diagram JSON (*.diagram.json);;All Files (*)", + _diagram_filter(), ) if not path: return @@ -441,7 +454,7 @@ def _save_as_diagram(self) -> None: self, _lang("diagram_editor_dialog_save", "Save Diagram"), "untitled.diagram.json", - "Diagram JSON (*.diagram.json);;All Files (*)", + _diagram_filter(), ) if not path: return @@ -541,7 +554,7 @@ def _warn_export_failed(self, path: str, reason: str) -> None: def _export_png(self) -> None: path, _ = QFileDialog.getSaveFileName( self, _lang("diagram_editor_dialog_export_png", "Export PNG"), - "diagram.png", "PNG Image (*.png)", + "diagram.png", _lang("diagram_editor_filter_png"), ) if not path: return @@ -577,7 +590,7 @@ def _export_png(self) -> None: def _export_svg(self) -> None: path, _ = QFileDialog.getSaveFileName( self, _lang("diagram_editor_dialog_export_svg", "Export SVG"), - "diagram.svg", "SVG Image (*.svg)", + "diagram.svg", _lang("diagram_editor_filter_svg"), ) if not path: return @@ -615,7 +628,7 @@ def _add_image_from_file(self) -> None: self, _lang("diagram_editor_dialog_image_file", "Open Image"), "", - "Images (*.png *.jpg *.jpeg *.bmp *.gif *.svg *.webp);;All Files (*)", + _image_filter(), ) if not path: return diff --git a/test/test_utils/test_diagram_editor_widget.py b/test/test_utils/test_diagram_editor_widget.py index aeedbce1..45c241b0 100644 --- a/test/test_utils/test_diagram_editor_widget.py +++ b/test/test_utils/test_diagram_editor_widget.py @@ -272,3 +272,47 @@ def test_the_dialog_goes_once_closed(self, editor, monkeypatch, accepted): for _ in range(3): editor._import_mermaid() assert self._dialogs_left(editor) == 0 + + +class TestFileDialogFilters: + """The dialogs' filters were English literals in every language.""" + + @staticmethod + def _filters(editor, monkeypatch) -> dict[str, str]: + from pybreeze.pybreeze_ui.diagram_editor import diagram_editor_widget + + asked: list[str] = [] + + def dialog(_parent, _title, _start, file_filter): + asked.append(file_filter) + return "", "" # cancelled + + for name in ("getOpenFileName", "getSaveFileName"): + monkeypatch.setattr(diagram_editor_widget.QFileDialog, name, staticmethod(dialog)) + seen: dict[str, str] = {} + for name, action in (("open", editor._open_diagram), ("save", editor._save_as_diagram), + ("png", editor._export_png), ("svg", editor._export_svg), + ("image", editor._add_image_from_file)): + action() + seen[name] = asked.pop() + return seen + + def test_they_speak_the_ide_language(self, editor, monkeypatch): + from je_editor import language_wrapper + + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + monkeypatch.setattr(language_wrapper, "language_word_dict", pybreeze_traditional_chinese_word_dict) + filters = self._filters(editor, monkeypatch) + + assert "所有檔案 (*)" in filters["open"] and "所有檔案 (*)" in filters["image"] + assert filters["open"] == filters["save"] + assert all("Image" not in text and "Files" not in text for text in filters.values()), filters + + def test_the_image_filter_offers_every_suffix_a_diagram_keeps(self, editor, monkeypatch): + # .ico was allowed in a saved diagram but not offered by Add Image + from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import IMAGE_SUFFIXES + + image_filter = self._filters(editor, monkeypatch)["image"] + assert all(f"*{suffix}" in image_filter for suffix in IMAGE_SUFFIXES) diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 9799407b..6db58bb4 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -92,11 +92,12 @@ def test_traditional_chinese_uses_taiwan_terms(self): def test_traditional_chinese_uses_full_width_punctuation(self): # Forty-two labels ended in ":" and a few in ":"; the SSH placeholder read - # "主機 (例如: ...)". A file dialog's filter keeps "(*.txt)": Qt reads it + # "主機 (例如: ...)". A file dialog's filter keeps "(*.txt)", or "({patterns})" + # filled in with them: Qt reads it cjk = "[一-鿿]" half_width = { key: value for key, value in ZH.items() - if re.search(rf"{cjk}\s?(:|\((?!\*))|:(\s*$|\s+\{{)|\((?!\*)[^)]*{cjk}", value) + if re.search(rf"{cjk}\s?(:|\((?![*{{]))|:(\s*$|\s+\{{)|\((?![*{{])[^)]*{cjk}", value) } assert half_width == {} From 878b5b5078a18f6c45be70363577f7b4c1418d2a Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:15:44 +0800 Subject: [PATCH 077/166] =?UTF-8?q?Say=20=E6=8F=90=E7=A4=BA=E8=A9=9E=20thr?= =?UTF-8?q?oughout=20and=20title=20the=20CoT=20review=20window=20as=20its?= =?UTF-8?q?=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++++++ docs/updates/README.md | 3 ++- pybreeze/extend_multi_language/extend_english.py | 2 +- .../extend_traditional_chinese.py | 14 +++++++------- test/test_utils/test_language_parity.py | 4 ++++ 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index d28e7a3f..75f0e0af 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -459,7 +459,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 139 個 `test_*.py`、2482 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 139 個 `test_*.py`、2483 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index d51e212f..52d256b9 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -233,3 +233,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2482 tests in 139 files. `ruff check` clean. The full suite at `d7d5d51` passed (2463 passed, 15 skipped). - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_diagram_editor_widget.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-26 · 2026-09-25 · 提示詞 throughout the Traditional Chinese IDE, and a CoT review window named as its tab · #fix #ai #i18n + +- **What**: the Traditional Chinese strings called a prompt 提示詞 in twelve places (the prompt editors' tabs, docks and menu entries) and Prompt or prompt in seven, sometimes on one screen: the CoT Prompt Editor tab (CoT 提示詞編輯器) is labelled "Prompt 檔案位置(會覆寫內建 prompt):" inside; Skill Send read "選擇 Prompt 範本:" and "Prompt:". The CoT Code Review panel's window title, shown when it stands alone, was "Prompt Sender UI" (Prompt 傳送介面), which names neither the tool nor its tab. +- **Fix**: the seven say 提示詞 (提示詞檔案位置(會覆寫內建提示詞):, 選擇提示詞範本:, 提示詞:, 請輸入 API URL 和提示詞, and the two messages); the window title is "CoT Code Review" (CoT 程式碼審查), its tab's name. +- **Tests**: `test_traditional_chinese_says_prompt_one_way` in `test_language_parity.py`: no Traditional Chinese string holds the word prompt. It failed on the seven before the change. With the AI panel, CoT and Skill Send tests, 90 pass. +- **Result / numbers**: 2483 tests in 139 files. `ruff check` clean. +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 80ccea8c..b184b2c6 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-26 | 2026-09-25 | 提示詞 throughout the Traditional Chinese IDE, and a CoT review window named as its tab | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-25 | 2026-09-25 | The diagram editor's file dialogs in the IDE's language, offering every image it keeps | #fix #diagram #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-24 | 2026-09-25 | Refactor: the diagram image suffix allowlist public, its comment back on it | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-23 | 2026-09-25 | Deleting and discarding ask with No as the default | #fix #ui #ssh #diagram | [2026-09-c](2026-09-c.md) | @@ -428,4 +429,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 25 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 26 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index bcba7305..c43407be 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -359,7 +359,7 @@ "extend_tools_menu_skill_prompt_send_dock_action": "Skill Send Dock", "extend_tools_menu_skill_prompt_send_dock_title": "Skill Send", # CoT code-review GUI - "cot_gui_window_title": "Prompt Sender UI", + "cot_gui_window_title": "CoT Code Review", "cot_gui_label_api_url": "API URL:", "cot_gui_placeholder_api_url": "The API URL to send to, e.g. https://llm.example.com/api", "cot_gui_placeholder_code_paste_area": "Paste the code to review here", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 1f051457..3ea01c33 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -146,7 +146,7 @@ "prthinker_setting_bad_extra_arguments": "額外參數無法解讀:有引號沒有關上。請補上或刪掉後再存檔。", "prthinker_setting_save_failed": "設定無法存到 {path},原本的設定保持不變。", # Prompt 編輯器 —— 編輯過的 prompt 會覆寫內建版本 - "prompt_editor_stored_at_label": "Prompt 檔案位置(會覆寫內建 prompt):", + "prompt_editor_stored_at_label": "提示詞檔案位置(會覆寫內建提示詞):", "prthinker_choose_source_path_label": "選擇 prthinker 原始碼資料夾", "prthinker_need_source_path_message": "prthinker 是從原始碼安裝的。請選擇含有 pyproject.toml 的資料夾," @@ -349,7 +349,7 @@ "extend_tools_menu_skill_prompt_send_dock_action": "Skill 提示詞傳送停駐窗格", "extend_tools_menu_skill_prompt_send_dock_title": "Skill 提示詞傳送", # CoT code-review GUI - "cot_gui_window_title": "Prompt 傳送介面", + "cot_gui_window_title": "CoT 程式碼審查", "cot_gui_label_api_url": "API URL:", "cot_gui_placeholder_api_url": "要傳送到的 API URL,例如 https://llm.example.com/api", "cot_gui_placeholder_code_paste_area": "在這裡貼上要審查的程式碼", @@ -367,14 +367,14 @@ "skills_exception": "發生例外:{error}", "skills_api_url_label": "LLM API URL:", "skills_api_url_placeholder": "要傳送到的 API URL,例如 https://llm.example.com/api", - "skills_prompt_select_label": "選擇 Prompt 範本:", - "skills_prompt_label": "Prompt:", + "skills_prompt_select_label": "選擇提示詞範本:", + "skills_prompt_label": "提示詞:", "skills_send_button": "傳送", "skills_response_label": "回傳結果:", - "skills_missing_input": "請輸入 API URL 和 Prompt", + "skills_missing_input": "請輸入 API URL 和提示詞", "skills_generating": "產生中...", - "skills_switch_over_edits": "Prompt 已經修改過。要換成 {name},並捨棄這些修改嗎?", - "skills_code_missing": "送出前,請把程式碼放在 prompt 裡 {code_diff} 的位置。", + "skills_switch_over_edits": "提示詞已經修改過。要換成 {name},並捨棄這些修改嗎?", + "skills_code_missing": "送出前,請把程式碼放在提示詞裡 {code_diff} 的位置。", # JupyterLab GUI "jupyterlab_init": "初始化中...", "jupyterlab_downloading": "下載中...", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 6db58bb4..7399f431 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -136,6 +136,10 @@ def test_a_dock_is_titled_like_the_tab_of_the_same_tool(self): assert words[f"extend_tools_menu_{tool}_dock_title"] == tab assert words[f"extend_tools_menu_{tool}_dock_action"].startswith(tab), tool + def test_traditional_chinese_says_prompt_one_way(self): + # The prompt editors' tabs said 提示詞 and their labels "Prompt 檔案位置(會覆寫內建 prompt)" + assert {key: value for key, value in ZH.items() if re.search(r"\b[Pp]rompt\b", str(value))} == {} + def test_a_name_is_spelled_one_way(self): # "Autocontrol" beside "AutoControl GUI" and "Install AutoControl"; # "Test Pioneer" beside "TestPioneer"; "Yaml" for YAML; "Install Automation File" From e013b78618b223093d7746732bbe18259add4855 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:25:09 +0800 Subject: [PATCH 078/166] Name the file a run window runs in its title --- architecture_explore.md | 8 ++- docs/updates/2026-09-c.md | 9 +++ docs/updates/README.md | 3 +- .../process_executor_utils.py | 9 ++- .../python_task_process_manager.py | 18 +++--- .../test_pioneer_process_manager.py | 3 +- test/test_utils/test_run_output.py | 7 ++- test/test_utils/test_run_window_title.py | 62 +++++++++++++++++++ 8 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 test/test_utils/test_run_window_title.py diff --git a/architecture_explore.md b/architecture_explore.md index 75f0e0af..dfb7621f 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -102,9 +102,11 @@ Template Method 定義的子行程生命週期: 三種啟動介面: -- `start_test_process(package, exec_str)` — 腳本內容直接走 `--execute_str`(Windows 上先 `json.dumps` 逃逸);Windows 上命令列超過 30,000 字元(上限 32,767)時改寫進暫存的 `pybreeze_run_*.json`、走 `--execute_file`(JSON 以全跳脫的 ASCII 寫回,套件用哪種編碼讀都一樣),執行結束或啟動失敗就刪掉 +- `start_test_process(package, exec_str, subject="")` — 腳本內容直接走 `--execute_str`(Windows 上先 `json.dumps` 逃逸);Windows 上命令列超過 30,000 字元(上限 32,767)時改寫進暫存的 `pybreeze_run_*.json`、走 `--execute_file`(JSON 以全跳脫的 ASCII 寫回,套件用哪種編碼讀都一樣),執行結束或啟動失敗就刪掉 - `start_test_process_file(package, file_path)` — 走 `--execute_file`,避開 Windows ~32K 命令列上限 -- `start_module_process(package, arguments, environment)` — 通用形式;**祕密(API key、token)走 environment 不走命令列**,工作管理員看不到 +- `start_module_process(package, arguments, environment, subject="")` — 通用形式;**祕密(API key、token)走 environment 不走命令列**,工作管理員看不到 + +執行視窗的標題是 `套件 - 檔名`(`subject`:編輯器分頁的檔名、`--execute_file` 的檔案、TestPioneer 的 YAML),沒有檔案時只有套件名;一次執行整個資料夾時,每個檔案一個視窗,這樣才分得出來 ### 4.2 `process_executor_utils.py` — 工廠函式 @@ -459,7 +461,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 139 個 `test_*.py`、2483 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 140 個 `test_*.py`、2487 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 52d256b9..ce31cd82 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -242,3 +242,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2483 tests in 139 files. `ruff check` clean. - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-27 · 2026-09-25 · A run window names the file it runs · #fix #executor #ui + +- **What**: every automation run window was titled with the package alone (`je_api_testka`), so running a folder, which opens one window per file, gave a row of windows nothing told apart; so did two runs of different tabs. The plugin run windows already read `Run - main.go`. +- **Change**: `TaskProcessManager` titles its window `package - subject` (`_spawn_and_pump`): `start_test_process_file` uses the file's name, `start_test_process` and `start_module_process` take an optional `subject`; `build_process` passes the name of the file the tab in front has open, and TestPioneer's run passes its YAML. A run with no file (a script given outright, pip, prthinker) keeps the package alone. +- **Tests**: new `test_run_window_title.py` (4), running `python -m this`: a file run, a tab's script and a module run with a subject name the file; a script from nowhere names the package. The three with a file failed before the change. `test_run_output.py`'s TestPioneer recorder takes and checks the subject. With the build, install, prthinker, closing, TestPioneer menu and compile-then-run tests, 77 pass. +- **Result / numbers**: 2487 tests in 140 files. `ruff check` clean; `C901` clean. +- **Files**: `pybreeze/extend/process_executor/python_task_process_manager.py`, `pybreeze/extend/process_executor/process_executor_utils.py`, `pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py`, `test/test_utils/test_run_window_title.py`, `test/test_utils/test_run_output.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b184b2c6..f0088b18 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-27 | 2026-09-25 | A run window names the file it runs | #fix #executor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-26 | 2026-09-25 | 提示詞 throughout the Traditional Chinese IDE, and a CoT review window named as its tab | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-25 | 2026-09-25 | The diagram editor's file dialogs in the IDE's language, offering every image it keeps | #fix #diagram #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-24 | 2026-09-25 | Refactor: the diagram image suffix allowlist public, its comment back on it | #refactor #diagram | [2026-09-c](2026-09-c.md) | @@ -429,4 +430,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 26 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 27 | diff --git a/pybreeze/extend/process_executor/process_executor_utils.py b/pybreeze/extend/process_executor/process_executor_utils.py index f6e638f0..e9e5b097 100644 --- a/pybreeze/extend/process_executor/process_executor_utils.py +++ b/pybreeze/extend/process_executor/process_executor_utils.py @@ -4,6 +4,7 @@ import time import weakref from collections.abc import Callable +from pathlib import Path from typing import TYPE_CHECKING from PySide6.QtCore import QCoreApplication, QObject, QTimer, Signal @@ -36,13 +37,15 @@ def build_process( reports its own errors in the run window. """ test_format_code = exec_str + subject = "" if test_format_code is None: widget = main_window.tab_widget.currentWidget() if not isinstance(widget, EditorWidget): report_no_script_tab(main_window, package, program_buffer) return test_format_code = widget.code_edit.toPlainText() - start_process(main_window, package, test_format_code, send_mail, program_buffer) + subject = Path(widget.current_file).name if widget.current_file else "" + start_process(main_window, package, test_format_code, send_mail, program_buffer, subject) def report_no_script_tab( @@ -66,12 +69,14 @@ def start_process( package: str, test_format_code: str, send_mail: bool = False, - program_buffer: int = 1024000 + program_buffer: int = 1024000, + subject: str = "", ): process = build_task_process(main_window, send_mail, program_buffer) process.start_test_process( package, exec_str=test_format_code, + subject=subject, ) diff --git a/pybreeze/extend/process_executor/python_task_process_manager.py b/pybreeze/extend/process_executor/python_task_process_manager.py index e41d29f4..902c0148 100644 --- a/pybreeze/extend/process_executor/python_task_process_manager.py +++ b/pybreeze/extend/process_executor/python_task_process_manager.py @@ -117,11 +117,13 @@ def renew_path(self) -> bool: self.compiler_path = self.main_window.python_compiler return True - def start_test_process(self, package: str, exec_str: str): + def start_test_process(self, package: str, exec_str: str, subject: str = ""): """Run *package* on the script *exec_str*, passed on the command line. A script too long for a Windows command line goes as a file instead (``--execute_file``): passed as it was, the run did not start at all. + *subject* -- the name of the file the script came from -- goes in the + window's title beside the package. """ if not self.renew_path(): return @@ -129,7 +131,7 @@ def start_test_process(self, package: str, exec_str: str): args = [str(self.compiler_path), "-m", package, "--execute_str", argument] if sys.platform == "win32" and len(subprocess.list2cmdline(args)) > _MAX_COMMAND_LINE: args[-2:] = ["--execute_file", str(self._write_script_file(exec_str))] - self._spawn_and_pump(package, args) + self._spawn_and_pump(package, args, subject=subject) def _write_script_file(self, script: str) -> Path: """Write *script* to a file of its own for the child to read, and return its path. @@ -167,24 +169,25 @@ def start_test_process_file(self, package: str, file_path: str): "--execute_file", str(file_path), ] - self._spawn_and_pump(package, args) + self._spawn_and_pump(package, args, subject=Path(file_path).name) def start_module_process( - self, package: str, arguments: list, environment: dict | None = None): + self, package: str, arguments: list, environment: dict | None = None, subject: str = ""): """Run ``python -m package`` with *arguments*, adding *environment* if given. The general form behind the two calls above, for a package driven by subcommands and flags rather than by a script to execute. A setting that would be a secret on a command line -- an API key, a forge token -- goes through *environment* instead, where the process list cannot show it. + *subject*, what the run is about (a file), goes in the window's title. """ if not self.renew_path(): return args = [str(self.compiler_path), "-m", package, *[str(one) for one in arguments]] - self._spawn_and_pump(package, args, environment) + self._spawn_and_pump(package, args, environment, subject) def _spawn_and_pump( - self, package: str, args: list, environment: dict | None = None) -> None: + self, package: str, args: list, environment: dict | None = None, subject: str = "") -> None: # Launch user-authored automation script in a child interpreter. # Argument list is validated upstream; shell=False, no user string ever # reaches a shell. nosec B603 — intentional local process execution. @@ -228,7 +231,8 @@ def _spawn_and_pump( daemon=True ) self.read_program_error_output_from_thread.start() - self.main_window.setWindowTitle(package) + # With the file: a folder run opens a window per file, all of one package + self.main_window.setWindowTitle(f"{package} - {subject}" if subject else package) self.main_window.show() self.timer = QTimer(self.main_window) self.timer.setInterval(100) diff --git a/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py b/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py index b1922de2..8debaca0 100644 --- a/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py +++ b/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_task_process @@ -19,4 +20,4 @@ def init_and_start_test_pioneer_process( than raised out of the menu callback. """ process = build_task_process(ui_we_want_to_set, program_buffer=program_buffer) - process.start_module_process(_PACKAGE, ["-e", file_path]) + process.start_module_process(_PACKAGE, ["-e", file_path], subject=Path(file_path).name) diff --git a/test/test_utils/test_run_output.py b/test/test_utils/test_run_output.py index 14d2c52e..c70f399f 100644 --- a/test/test_utils/test_run_output.py +++ b/test/test_utils/test_run_output.py @@ -221,8 +221,8 @@ def test_runs_the_chosen_yaml_through_the_task_manager(self, monkeypatch): calls: list = [] class Recorder: - def start_module_process(self, package, arguments, environment=None): - calls.append((package, list(arguments), environment)) + def start_module_process(self, package, arguments, environment=None, subject=""): + calls.append((package, list(arguments), environment, subject)) monkeypatch.setattr( test_pioneer_process_manager, "build_task_process", @@ -231,7 +231,8 @@ def start_module_process(self, package, arguments, environment=None): test_pioneer_process_manager.init_and_start_test_pioneer_process( MainWindow(), "C:/tests/run.yml") - assert calls == [("test_pioneer", ["-e", "C:/tests/run.yml"], None)] + # The run window is titled with the file's name + assert calls == [("test_pioneer", ["-e", "C:/tests/run.yml"], None, "run.yml")] def test_no_interpreter_is_reported_not_raised(self, qt_app, monkeypatch): from je_editor import JEditorExecException diff --git a/test/test_utils/test_run_window_title.py b/test/test_utils/test_run_window_title.py new file mode 100644 index 00000000..20f56bdc --- /dev/null +++ b/test/test_utils/test_run_window_title.py @@ -0,0 +1,62 @@ +"""A run window says what it runs: the package, and the file when there is one. + +Running a folder opens one window per file, and every one was titled with the +package alone (``je_api_testka``), so nothing told them apart; the plugin run +windows already read ``Run - main.go``. +""" +from __future__ import annotations + +import os +import sys +import time + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest + +# A module that runs, ignores its arguments and ends at once +_PACKAGE = "this" + + +@pytest.fixture(scope="module") +def app(): + from PySide6.QtWidgets import QApplication + + return QApplication.instance() or QApplication([]) + + +@pytest.fixture() +def manager(app): + from pybreeze.extend.process_executor.python_task_process_manager import TaskProcessManager + from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow + + window = CodeWindow() + runner = TaskProcessManager(window) + runner.renew_path = lambda: True + runner.compiler_path = sys.executable + yield runner + deadline = time.monotonic() + 30 + while runner.still_run_program and time.monotonic() < deadline: + app.processEvents() + time.sleep(0.02) + window.close() + + +def test_a_file_run_names_the_file(manager, tmp_path): + manager.start_test_process_file(_PACKAGE, str(tmp_path / "login_flow.json")) + assert manager.main_window.windowTitle() == f"{_PACKAGE} - login_flow.json" + + +def test_a_script_run_names_the_tab_it_came_from(manager): + manager.start_test_process(_PACKAGE, "[]", subject="checkout.json") + assert manager.main_window.windowTitle() == f"{_PACKAGE} - checkout.json" + + +def test_a_script_from_nowhere_names_the_package(manager): + manager.start_test_process(_PACKAGE, "[]") + assert manager.main_window.windowTitle() == _PACKAGE + + +def test_a_module_run_names_what_it_was_given(manager): + manager.start_module_process(_PACKAGE, ["-e", "suite.yml"], subject="suite.yml") + assert manager.main_window.windowTitle() == f"{_PACKAGE} - suite.yml" From a7412e201286a51d92ec7d4caf69bca6a5fddfd8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:25:22 +0800 Subject: [PATCH 079/166] Count the dictionary keys in the READMEs and test the count --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++++++ docs/updates/README.md | 3 ++- test/test_utils/test_language_parity.py | 14 ++++++++++++++ 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cb575131..7f6cff9b 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 735 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 753 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 8703385b..42a7fb9c 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -211,7 +211,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 735 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 753 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 8852c9ce..fdfa1a00 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -211,7 +211,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 735 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 753 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index dfb7621f..541fdd8b 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -461,7 +461,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 140 個 `test_*.py`、2487 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 140 個 `test_*.py`、2488 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ce31cd82..a1d5b258 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -251,3 +251,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2487 tests in 140 files. `ruff check` clean; `C901` clean. - **Files**: `pybreeze/extend/process_executor/python_task_process_manager.py`, `pybreeze/extend/process_executor/process_executor_utils.py`, `pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py`, `test/test_utils/test_run_window_title.py`, `test/test_utils/test_run_output.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-28 · 2026-09-25 · The READMEs count the dictionary keys there are, and a test keeps them to it · #docs #i18n #readme + +- **What**: the three READMEs said both language dictionaries carry the same 735 keys; they hold 753. The number was kept by hand and had already drifted before this batch's own additions and removals. +- **Change**: 753 in `README.md`, `README/README_zh-TW.md` and `README/README_zh-CN.md`. +- **Tests**: `test_the_readmes_count_the_keys_there_are` in `test_language_parity.py` reads the count from each README and fails when it is not the size of the two dictionaries, so a key added or removed without the READMEs fails the suite. It failed on 735 before the change. `test_language_parity.py` passes (25). +- **Result / numbers**: 2488 tests in 140 files. `ruff check` clean. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f0088b18..0e7516a5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-28 | 2026-09-25 | The READMEs count the dictionary keys there are, and a test keeps them to it | #docs #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-27 | 2026-09-25 | A run window names the file it runs | #fix #executor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-26 | 2026-09-25 | 提示詞 throughout the Traditional Chinese IDE, and a CoT review window named as its tab | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-25 | 2026-09-25 | The diagram editor's file dialogs in the IDE's language, offering every image it keeps | #fix #diagram #i18n | [2026-09-c](2026-09-c.md) | @@ -430,4 +431,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 27 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 28 | diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index 7399f431..e2620d5d 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -285,3 +285,17 @@ def test_the_window_is_called_pybreeze_in_every_language(self): finally: language_wrapper.reset_language(original) assert set(names.values()) == {"PyBreeze"}, names + + +def test_the_readmes_count_the_keys_there_are(): + # They said 735 while the dictionaries held 753: a count kept by hand drifts + readmes = { + "README.md": r"the same (\d+) keys", + "README/README_zh-TW.md": r"相同的 (\d+) 個鍵", + "README/README_zh-CN.md": r"同样的 (\d+) 个键", + } + root = pathlib.Path(pybreeze.__file__).parent.parent + for name, pattern in readmes.items(): + found = re.search(pattern, (root / name).read_text(encoding="utf-8")) + assert found is not None, name + assert int(found.group(1)) == len(EN) == len(ZH), name From 79d83829186005332ed768a5050c2e2d483f7675 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:26:53 +0800 Subject: [PATCH 080/166] Re-measure the counts in the architecture map --- architecture_explore.md | 8 ++++---- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- test/test_utils/test_language_parity.py | 4 +++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 541fdd8b..d7ad6990 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -1,6 +1,6 @@ # PyBreeze 架構探勘 / Architecture Exploration -> 掃描範圍:`pybreeze/`(201 個 `.py`、約 22,200 行,不含空行與註解約 17,300 行)+ `test/`、`exe/`、`docs/`、CI 設定 +> 掃描範圍:`pybreeze/`(208 個 `.py`、約 24,000 行,不含空行與註解約 19,400 行)+ `test/`、`exe/`、`docs/`、CI 設定 > 對應版本:`pyproject.toml` 1.0.21(stable)/`dev.toml` 1.0.14(dev),分支 `dev` --- @@ -242,7 +242,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,018 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,034 行,最大子系統) | 檔案 | 職責 | |---|---| @@ -297,7 +297,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 9. `pybreeze_ui/connect_gui/` -### `ssh/`(2,230 行) +### `ssh/`(2,467 行) | 檔案 | 職責 | |---|---| @@ -368,7 +368,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 735 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 753 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index a1d5b258..01e6318d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -260,3 +260,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2488 tests in 140 files. `ruff check` clean. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-29 · 2026-09-25 · Re-measure the counts the architecture map quotes · #docs + +- **What**: after U-20260925-28, the counts `architecture_explore.md` quotes were measured again, as the project rules ask. Four had drifted: the package was 201 `.py` files and about 22,200 lines (17,300 without blank lines and comments), the diagram editor 4,018 lines, `ssh/` 2,230 lines, and each language dictionary 735 keys. +- **Result / numbers**: 208 `.py` files, 24,003 lines, 19,404 without blank lines and comments (tokenized; docstrings count as code); `diagram_editor/` 4,034 lines; `connect_gui/ssh/` 2,467 lines; 753 keys. Unchanged and confirmed: 13 tool widgets in `tools_gui/`, 20 widget factories, 18 `utils/` subpackages, 17 `ITEException` classes, `syntax_keyword.py` 629 lines. +- **Tests**: `test_the_readmes_count_the_keys_there_are` checks the map's key count too. `test_language_parity.py` passes (25). +- **Files**: `architecture_explore.md`, `test/test_utils/test_language_parity.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0e7516a5..6ce6372e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-29 | 2026-09-25 | Re-measure the counts the architecture map quotes | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-28 | 2026-09-25 | The READMEs count the dictionary keys there are, and a test keeps them to it | #docs #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-27 | 2026-09-25 | A run window names the file it runs | #fix #executor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-26 | 2026-09-25 | 提示詞 throughout the Traditional Chinese IDE, and a CoT review window named as its tab | #fix #ai #i18n | [2026-09-c](2026-09-c.md) | @@ -431,4 +432,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 28 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 29 | diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index e2620d5d..d217a77c 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -288,11 +288,13 @@ def test_the_window_is_called_pybreeze_in_every_language(self): def test_the_readmes_count_the_keys_there_are(): - # They said 735 while the dictionaries held 753: a count kept by hand drifts + # They said 735 while the dictionaries held 753: a count kept by hand drifts. + # The architecture map quotes it too readmes = { "README.md": r"the same (\d+) keys", "README/README_zh-TW.md": r"相同的 (\d+) 個鍵", "README/README_zh-CN.md": r"同样的 (\d+) 个键", + "architecture_explore.md": r"各 (\d+) 個鍵", } root = pathlib.Path(pybreeze.__file__).parent.parent for name, pattern in readmes.items(): From f4d7c5868575af5f3dc2ec5ece4a5b34a22c8d4c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:30:33 +0800 Subject: [PATCH 081/166] Label the SSH secret field Passphrase under key authentication --- README.md | 4 ++-- README/README_zh-CN.md | 4 ++-- README/README_zh-TW.md | 4 ++-- architecture_explore.md | 4 ++-- docs/updates/2026-09-c.md | 10 +++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_english.py | 4 +++- .../extend_traditional_chinese.py | 4 +++- .../connect_gui/ssh/ssh_login_widget.py | 18 ++++++++++++--- test/test_utils/test_ssh_login_form.py | 22 +++++++++++++++++++ 10 files changed, 63 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7f6cff9b..86da7544 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included; a PuTTY `.ppk` key is exported from PuTTYgen as an OpenSSH key first, as the error message says), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included; a PuTTY `.ppk` key is exported from PuTTYgen as an OpenSSH key first, as the error message says; with key authentication the password field reads Passphrase and takes the key's passphrase), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also @@ -211,7 +211,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 753 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 755 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 42a7fb9c..db37b7a8 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -149,7 +149,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以;PuTTY 的 `.ppk` 密钥需先在 PuTTYgen 导出为 OpenSSH 密钥,错误信息会说明如何操作),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以;PuTTY 的 `.ppk` 密钥需先在 PuTTYgen 导出为 OpenSSH 密钥,错误信息会说明如何操作;勾选密钥认证时,密码栏会改为“密语”,填入私钥的密语),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 @@ -211,7 +211,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 753 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 755 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index fdfa1a00..245578bb 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -149,7 +149,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以;PuTTY 的 `.ppk` 金鑰要先在 PuTTYgen 匯出成 OpenSSH 金鑰,錯誤訊息會說明怎麼做),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以;PuTTY 的 `.ppk` 金鑰要先在 PuTTYgen 匯出成 OpenSSH 金鑰,錯誤訊息會說明怎麼做;勾選金鑰驗證時,密碼欄會改成「密語」,填入私鑰的密語),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 @@ -211,7 +211,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 753 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 755 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index d7ad6990..3630d56d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -368,7 +368,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 753 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 755 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -461,7 +461,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 140 個 `test_*.py`、2488 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 140 個 `test_*.py`、2491 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 01e6318d..dc98e4bc 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -268,3 +268,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Tests**: `test_the_readmes_count_the_keys_there_are` checks the map's key count too. `test_language_parity.py` passes (25). - **Files**: `architecture_explore.md`, `test/test_utils/test_language_parity.py` - **Open items**: none. + +## U-20260925-30 · 2026-09-25 · The SSH login form's secret field says Passphrase for a key · #fix #ssh #i18n #readme + +- **What**: with Use key auth ticked, the SSH login form's Password field holds the private key's passphrase (`load_private_key` reads it so), but it still read "Password", and a key that needed one was answered with "enter it in the password field". +- **Change**: `LoginWidget._name_the_secret`, run as the box is ticked or unticked, labels the field Passphrase (密語) with the placeholder "The key's passphrase, if it has one" (私鑰的密語(沒有就留空)), and Password again when it is unticked. The missing-passphrase message says the Passphrase field (密語欄). The README's screenshot, taken unticked, is unchanged. +- **Tests**: `TestTheSecretField` in `test_ssh_login_form.py` (3): Password at first, Passphrase with key authentication, Password again when unticked; the last two failed before the change. `test_the_readmes_count_the_keys_there_are` (U-20260925-28) failed on the two new keys until the counts were updated: its first catch. With the language, SSH security and session-sharing tests, 68 pass. +- **Result / numbers**: 2491 tests in 140 files; 755 keys in each dictionary. `ruff check` clean. +- **Docs**: the SSH Client paragraph of the three READMEs; the key count in them and in `architecture_explore.md`. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_login_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ssh_login_form.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6ce6372e..a4a5104e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-30 | 2026-09-25 | The SSH login form's secret field says Passphrase for a key | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-29 | 2026-09-25 | Re-measure the counts the architecture map quotes | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-28 | 2026-09-25 | The READMEs count the dictionary keys there are, and a test keeps them to it | #docs #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-27 | 2026-09-25 | A run window names the file it runs | #fix #executor #ui | [2026-09-c](2026-09-c.md) | @@ -432,4 +433,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 29 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 30 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index c43407be..00b47cf0 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -176,7 +176,7 @@ "ssh_command_widget_dialog_title_key_error": "Key error", "ssh_command_widget_dialog_message_key_file_not_exist": "Key file does not exist.", "ssh_command_widget_error_message_unsupported_private_key": "Unsupported or invalid private key.", - "ssh_key_error_passphrase_needed": "The private key is protected by a passphrase: enter it in the password field.", + "ssh_key_error_passphrase_needed": "The private key is protected by a passphrase: enter it in the Passphrase field.", "ssh_key_error_passphrase_wrong": "The passphrase for the private key is wrong.", "ssh_key_error_putty_key": "This is a PuTTY key (.ppk), which cannot be used here: load it in PuTTYgen, " "choose Conversions > Export OpenSSH key, and pick the exported file.", @@ -272,9 +272,11 @@ "ssh_login_widget_label_user": "User", "ssh_login_widget_label_key": "Key", "ssh_login_widget_label_password": "Password", + "ssh_login_widget_label_passphrase": "Passphrase", "ssh_login_widget_placeholder_host": "Host name or IP address", "ssh_login_widget_placeholder_username": "Username", "ssh_login_widget_placeholder_password": "Password", + "ssh_login_widget_placeholder_passphrase": "The key's passphrase, if it has one", "ssh_login_widget_placeholder_private_key": "Private key path (OpenSSH or PEM)", "ssh_login_widget_button_use_key_auth": "Use key auth", "ssh_login_widget_button_connect": "Connect", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 3ea01c33..8a80e0b4 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -169,7 +169,7 @@ "ssh_command_widget_dialog_title_key_error": "金鑰錯誤", "ssh_command_widget_dialog_message_key_file_not_exist": "金鑰檔案不存在。", "ssh_command_widget_error_message_unsupported_private_key": "不支援或無效的私鑰。", - "ssh_key_error_passphrase_needed": "這把私鑰有密語保護:請在密碼欄輸入密語。", + "ssh_key_error_passphrase_needed": "這把私鑰有密語保護:請在密語欄輸入。", "ssh_key_error_passphrase_wrong": "私鑰的密語不對。", "ssh_key_error_putty_key": "這是 PuTTY 金鑰(.ppk),這裡無法使用:請在 PuTTYgen 載入它," "選 Conversions > Export OpenSSH key 匯出,再選擇匯出的檔案。", @@ -262,9 +262,11 @@ "ssh_login_widget_label_user": "使用者", "ssh_login_widget_label_key": "金鑰", "ssh_login_widget_label_password": "密碼", + "ssh_login_widget_label_passphrase": "密語", "ssh_login_widget_placeholder_host": "主機名稱或 IP 位址", "ssh_login_widget_placeholder_username": "使用者名稱", "ssh_login_widget_placeholder_password": "密碼", + "ssh_login_widget_placeholder_passphrase": "私鑰的密語(沒有就留空)", "ssh_login_widget_placeholder_private_key": "私鑰路徑(OpenSSH 或 PEM)", "ssh_login_widget_button_use_key_auth": "使用金鑰驗證", "ssh_login_widget_button_connect": "連線", diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_login_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_login_widget.py index fde352eb..7f59dfe7 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_login_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_login_widget.py @@ -26,6 +26,8 @@ def __init__(self, parent=None): self.connect_btn = QPushButton(language_wrapper.language_word_dict.get("ssh_login_widget_button_connect")) self.disconnect_btn = QPushButton(language_wrapper.language_word_dict.get("ssh_login_widget_button_disconnect")) self.status_label = QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_status_disconnected")) + # The password, or with key authentication the key's passphrase (see _name_the_secret) + self.pass_label = QLabel() # 初始化 UI self._setup_ui() @@ -37,9 +39,9 @@ def _setup_ui(self): self.port_spin.setValue(22) self.user_edit.setPlaceholderText( language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_username")) - self.pass_edit.setPlaceholderText( - language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_password")) self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password) + self._name_the_secret(False) + self.use_key_check.toggled.connect(self._name_the_secret) self.key_edit.setPlaceholderText( language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_private_key")) self.browse_key_btn.clicked.connect(self.choose_key_file) @@ -61,7 +63,7 @@ def _setup_ui(self): auth.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_key"))) auth.addWidget(self.key_edit) auth.addWidget(self.browse_key_btn) - auth.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_password"))) + auth.addWidget(self.pass_label) auth.addWidget(self.pass_edit) conn = QHBoxLayout() @@ -76,6 +78,16 @@ def _setup_ui(self): self.setLayout(root) + def _name_the_secret(self, key_auth: bool) -> None: + """Label the secret field for what it holds: the password, or with *key_auth* the key's passphrase.""" + word = language_wrapper.language_word_dict + if key_auth: + label, placeholder = "ssh_login_widget_label_passphrase", "ssh_login_widget_placeholder_passphrase" + else: + label, placeholder = "ssh_login_widget_label_password", "ssh_login_widget_placeholder_password" + self.pass_label.setText(word.get(label)) + self.pass_edit.setPlaceholderText(word.get(placeholder)) + def choose_key_file(self) -> None: """Pick the private key file in a dialog, starting in ``~/.ssh``; it also ticks key authentication.""" start = Path.home() / ".ssh" diff --git a/test/test_utils/test_ssh_login_form.py b/test/test_utils/test_ssh_login_form.py index 8fb03273..ccf19e89 100644 --- a/test/test_utils/test_ssh_login_form.py +++ b/test/test_utils/test_ssh_login_form.py @@ -71,3 +71,25 @@ def test_cancelling_changes_nothing(self, form, monkeypatch): assert form.key_edit.text() == "typed" assert not form.use_key_check.isChecked() + + +class TestTheSecretField: + """With key authentication the password field holds the key's passphrase, and says so.""" + + def _words(self): + from je_editor import language_wrapper + return language_wrapper.language_word_dict + + def test_it_asks_for_the_password_at_first(self, form): + assert form.pass_label.text() == self._words().get("ssh_login_widget_label_password") + + def test_key_authentication_makes_it_the_passphrase(self, form): + form.use_key_check.setChecked(True) + assert form.pass_label.text() == self._words().get("ssh_login_widget_label_passphrase") + assert form.pass_edit.placeholderText() == self._words().get("ssh_login_widget_placeholder_passphrase") + + def test_unticking_it_gives_the_password_back(self, form): + form.use_key_check.setChecked(True) + form.use_key_check.setChecked(False) + assert form.pass_label.text() == self._words().get("ssh_login_widget_label_password") + assert form.pass_edit.placeholderText() == self._words().get("ssh_login_widget_placeholder_password") From 60e8e524dd225f36420471812da0dd1e768460e1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:33:58 +0800 Subject: [PATCH 082/166] Run a tool with Ctrl+Enter --- README.md | 2 + README/README_zh-CN.md | 2 + README/README_zh-TW.md | 2 + architecture_explore.md | 7 +- docs/updates/2026-09-c.md | 10 +++ docs/updates/README.md | 3 +- .../pybreeze_ui/tools_gui/curl_import_gui.py | 2 + pybreeze/pybreeze_ui/tools_gui/diff_gui.py | 2 + pybreeze/pybreeze_ui/tools_gui/hash_gui.py | 2 + .../tools_gui/header_analyzer_gui.py | 2 + .../pybreeze_ui/tools_gui/json_format_gui.py | 2 + .../pybreeze_ui/tools_gui/jwt_decoder_gui.py | 2 + pybreeze/pybreeze_ui/tools_gui/regex_gui.py | 2 + .../tools_gui/response_inspector_gui.py | 2 + .../pybreeze_ui/tools_gui/run_shortcut.py | 25 ++++++ test/test_utils/test_tool_run_shortcut.py | 88 +++++++++++++++++++ 16 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 pybreeze/pybreeze_ui/tools_gui/run_shortcut.py create mode 100644 test/test_utils/test_tool_run_shortcut.py diff --git a/README.md b/README.md index 86da7544..aa76460a 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w ## Built-in Tools +In a tool with one main button, Ctrl+Enter anywhere in it presses that button: its text boxes take Enter as a new line. + ### cURL Import — a copied request becomes a runnable script Paste a `curl` command from your browser's dev tools and pick a target. The parser handles method, URL, headers, bodies, basic auth, `-G` query parameters, `-F` multipart fields (uploads become `files=open(...)`), the `--json` shortcut, `-d @file` bodies and multi-line continuations. Repeated `-H` values are combined the way HTTP combines them (`; ` for cookies, `, ` otherwise) instead of the last one silently winning. Nothing is ever executed — it is pure parsing. diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index db37b7a8..199885a7 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -83,6 +83,8 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ## 内置工具 +只有一个主要按钮的工具,在工具里任何地方按 Ctrl+Enter 就等于按下那个按钮:文本框里的 Enter 是换行。 + ### cURL 导入——复制的请求变成可运行的脚本 从浏览器开发者工具粘贴一条 `curl` 命令,选择目标格式。解析器能处理方法、URL、请求头、请求体、basic auth、`-G` 查询参数、`-F` multipart 字段(上传会变成 `files=open(...)`)、`--json` 简写、`-d @file` 请求体以及多行续行。重复的 `-H` 值会按照 HTTP 的方式合并(cookie 用 `; `,其他用 `, `),而不是悄悄只保留最后一个。它从不执行任何东西——纯粹只做解析。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 245578bb..a9907d85 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -83,6 +83,8 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ## 內建工具 +只有一個主要按鈕的工具,在工具裡任何地方按 Ctrl+Enter 就等於按下那顆按鈕:文字框裡的 Enter 是換行。 + ### cURL 匯入 — 複製下來的請求變成可執行的腳本 從瀏覽器開發者工具貼上一段 `curl` 指令,再選擇輸出目標。解析器能處理方法、URL、標頭、本文、Basic 驗證、`-G` 查詢參數、`-F` multipart 欄位(上傳檔案會變成 `files=open(...)`)、`--json` 簡寫、`-d @file` 本文,以及多行接續。重複的 `-H` 值會照 HTTP 的方式合併(cookie 用 `; `,其他用 `, `),而不是默默只留下最後一個。過程中不會執行任何東西——純粹是解析。 diff --git a/architecture_explore.md b/architecture_explore.md index 3630d56d..376af015 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -1,6 +1,6 @@ # PyBreeze 架構探勘 / Architecture Exploration -> 掃描範圍:`pybreeze/`(208 個 `.py`、約 24,000 行,不含空行與註解約 19,400 行)+ `test/`、`exe/`、`docs/`、CI 設定 +> 掃描範圍:`pybreeze/`(209 個 `.py`、約 24,000 行,不含空行與註解約 19,400 行)+ `test/`、`exe/`、`docs/`、CI 設定 > 對應版本:`pyproject.toml` 1.0.21(stable)/`dev.toml` 1.0.14(dev),分支 `dev` --- @@ -215,7 +215,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 6. 工具分頁 `pybreeze_ui/tools_gui/`(13 個工具 widget + 2 個共用機制) +## 6. 工具分頁 `pybreeze_ui/tools_gui/`(13 個工具 widget + 3 個共用機制) 每個工具都是 `QWidget`,UI 極薄,真正邏輯全在 `pybreeze/utils/` 對應的純函式套件裡(所以測得動、也測了)。 @@ -238,6 +238,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 兩個橫向共用機制 - **`tool_tabs.open_tool_tab()`** — 工具之間互相「轉交」:Response Inspector 把狀態碼丟給 HTTP Status、headers 丟給 Header Analyzer、JWT 丟給 JWT Decoder、JSON body 丟給 JSON Format;curl 匯入把 URL 丟給 URL Builder。開新分頁並自動聚焦。 +- **`run_shortcut.press_on_ctrl_enter()`** — 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發 - **`output_actions.OutputActions`** — 統一的「複製 / 在編輯器開啟 / 存檔」三顆按鈕,綁在工具的唯讀輸出 `QTextEdit` 上,輸出也經 `exact_text()` 讀(不讓 U+00A0、U+2028 被改掉);副檔名與檔名可傳 callable 動態決定。存檔經 `replace_text()` 整檔替換,失敗(唯讀資料夾、被鎖住的檔案、磁碟滿)時原檔不動、會跳警告,說出檔名與原因。Qt 的文字元件留不住貼上的 CR,換行一律讀成 LF --- @@ -461,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 140 個 `test_*.py`、2491 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2501 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index dc98e4bc..d3b1852a 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -278,3 +278,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the SSH Client paragraph of the three READMEs; the key count in them and in `architecture_explore.md`. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_login_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_ssh_login_form.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-31 · 2026-09-25 · Ctrl+Enter runs a tool · #feature #tools #readme + +- **What**: the tool tabs had no key to run them. Their inputs are multi-line text boxes, where Enter is a new line, so pasting a curl command, a response or two texts to diff and then running the tool took the mouse; only the regex pattern and the timestamp fields, single lines, ran on Enter. +- **Change**: new `tools_gui/run_shortcut.py`: `press_on_ctrl_enter(tool, button)` gives the tool two shortcuts, Ctrl+Return and Ctrl+Enter (the keypad's), that click the button, and names the keys in its tooltip. They are the tool's children with `WidgetWithChildrenShortcut`, so they apply only while the focus is in that tool (a docked tool does not take the keys from the editor, and JEditor binds neither), and a disabled button, a run still going, is not pressed. Wired into the eight tools with one main button: cURL Import (Convert), Text Diff (Compare), Hash, Header Analyzer, JSON Format (Format), JWT Decoder, Regex (Find matches) and Response Inspector. Query ↔ JSON and URL Parser / Builder have two directions and HAR Import two buttons, so they have none. +- **Tests**: new `test_tool_run_shortcut.py` (10): each of the eight tools has the two shortcuts, scoped to it, pressing its button and named in the tooltip; a disabled button is not pressed; a real Ctrl+Enter typed in Hash's text box computes the digests. All failed before the change. +- **Result / numbers**: 2501 tests in 141 files. `ruff check` clean. The full suite at `79d8382` passed (2473 passed, 15 skipped). +- **Docs**: the Built-in Tools section of the three READMEs; `architecture_explore.md` (§6's shared mechanisms, file and test counts). +- **Files**: `pybreeze/pybreeze_ui/tools_gui/run_shortcut.py`, the eight tools' `*_gui.py` in `pybreeze/pybreeze_ui/tools_gui/`, `test/test_utils/test_tool_run_shortcut.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a4a5104e..84e0d2f9 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-31 | 2026-09-25 | Ctrl+Enter runs a tool | #feature #tools #readme | [2026-09-c](2026-09-c.md) | | U-20260925-30 | 2026-09-25 | The SSH login form's secret field says Passphrase for a key | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-29 | 2026-09-25 | Re-measure the counts the architecture map quotes | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-28 | 2026-09-25 | The READMEs count the dictionary keys there are, and a test keeps them to it | #docs #i18n #readme | [2026-09-c](2026-09-c.md) | @@ -433,4 +434,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 30 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 31 | diff --git a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py index 31350d56..f67bd5e9 100644 --- a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py @@ -14,6 +14,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.header_analyzer_gui import HeaderAnalyzerGUI from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions @@ -62,6 +63,7 @@ def __init__(self, main_window=None) -> None: self.convert_button = QPushButton() self._name_convert_button() self.convert_button.clicked.connect(self.convert) + press_on_ctrl_enter(self, self.convert_button) self.output_label = QLabel(word.get("curl_import_output_label")) self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py index f11a12a1..43a0b4fd 100644 --- a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py @@ -9,6 +9,7 @@ from je_editor import language_wrapper from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.pybreeze_ui.thread_keeper import let_run_out @@ -107,6 +108,7 @@ def __init__(self, main_window=None) -> None: self.compare_button = QPushButton(word.get("diff_compare_button")) self.compare_button.clicked.connect(self.compare) + press_on_ctrl_enter(self, self.compare_button) self.summary_label = QLabel("") self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/hash_gui.py b/pybreeze/pybreeze_ui/tools_gui/hash_gui.py index cdc98ee1..c4733c4a 100644 --- a/pybreeze/pybreeze_ui/tools_gui/hash_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/hash_gui.py @@ -4,6 +4,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.hash_tools.hash_text import hash_all @@ -35,6 +36,7 @@ def __init__(self, main_window=None) -> None: self.hash_button = QPushButton(word.get("hash_button")) self.hash_button.clicked.connect(self.compute) + press_on_ctrl_enter(self, self.hash_button) self.output_label = QLabel(word.get("hash_output_label")) self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py b/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py index 362d3963..6f08ca4c 100644 --- a/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py @@ -12,6 +12,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.jwt_decoder_gui import JwtDecoderGUI from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions @@ -75,6 +76,7 @@ def __init__(self, main_window=None, initial_headers: str | None = None) -> None self.analyze_button = QPushButton(word.get("header_analyzer_analyze_button")) self.analyze_button.clicked.connect(self.analyze) + press_on_ctrl_enter(self, self.analyze_button) self.output_label = QLabel(word.get("header_analyzer_output_label")) self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py index 1b9965e0..7a0a1c43 100644 --- a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py @@ -6,6 +6,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.exception.exceptions import ITEJsonException @@ -36,6 +37,7 @@ def __init__(self, main_window=None, initial_json: str | None = None) -> None: self.format_button = QPushButton(word.get("json_format_format_button")) self.format_button.clicked.connect(self.format_json) + press_on_ctrl_enter(self, self.format_button) self.minify_button = QPushButton(word.get("json_format_minify_button")) self.minify_button.clicked.connect(self.minify) diff --git a/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py b/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py index 524844d0..750d9fc6 100644 --- a/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py @@ -9,6 +9,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.exception.exceptions import JwtDecodeException @@ -62,6 +63,7 @@ def __init__(self, initial_token: str | None = None, main_window=None) -> None: self.decode_button = QPushButton(word.get("jwt_decoder_decode_button")) self.decode_button.clicked.connect(self.decode) + press_on_ctrl_enter(self, self.decode_button) self.output_label = QLabel(word.get("jwt_decoder_output_label")) self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py index a7af16a2..e6b018b4 100644 --- a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py @@ -8,6 +8,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.thread_keeper import let_run_out from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions @@ -94,6 +95,7 @@ def __init__(self, main_window=None) -> None: self.test_button = QPushButton(word.get("regex_test_button")) self.test_button.clicked.connect(self.test) + press_on_ctrl_enter(self, self.test_button) self.output_label = QLabel(word.get("regex_output_label")) self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py index e741c901..8705ca13 100644 --- a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py @@ -11,6 +11,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.header_analyzer_gui import HeaderAnalyzerGUI from pybreeze.pybreeze_ui.tools_gui.http_status_gui import HttpStatusGUI, status_heading @@ -96,6 +97,7 @@ def __init__(self, main_window=None) -> None: self.analyze_button = QPushButton(word.get("response_analyze_button")) self.analyze_button.clicked.connect(self.analyze) + press_on_ctrl_enter(self, self.analyze_button) self.output_label = QLabel(word.get("response_output_label")) self.output_edit = QTextEdit() diff --git a/pybreeze/pybreeze_ui/tools_gui/run_shortcut.py b/pybreeze/pybreeze_ui/tools_gui/run_shortcut.py new file mode 100644 index 00000000..db6a5038 --- /dev/null +++ b/pybreeze/pybreeze_ui/tools_gui/run_shortcut.py @@ -0,0 +1,25 @@ +"""Ctrl+Enter runs a tool: its text boxes take Enter as a new line.""" +from __future__ import annotations + +from PySide6.QtCore import Qt +from PySide6.QtGui import QKeySequence, QShortcut +from PySide6.QtWidgets import QAbstractButton, QWidget + +# The main keyboard's Enter and the keypad's +RUN_KEYS = ("Ctrl+Return", "Ctrl+Enter") +# How the button's tooltip names them +RUN_KEYS_SHOWN = "Ctrl+Enter" + + +def press_on_ctrl_enter(tool: QWidget, button: QAbstractButton) -> None: + """Let Ctrl+Enter anywhere in *tool* press *button*, as a click would. + + A disabled button (a run still going) is not pressed. The shortcuts are + children of *tool*, so they go with it, and apply only while the focus is + in it: a tool in a dock does not take the keys from the editor. + """ + for keys in RUN_KEYS: + shortcut = QShortcut(QKeySequence(keys), tool) + shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) + shortcut.activated.connect(button.click) + button.setToolTip(RUN_KEYS_SHOWN) diff --git a/test/test_utils/test_tool_run_shortcut.py b/test/test_utils/test_tool_run_shortcut.py new file mode 100644 index 00000000..9b1867bd --- /dev/null +++ b/test/test_utils/test_tool_run_shortcut.py @@ -0,0 +1,88 @@ +"""Ctrl+Enter runs a tool: its text boxes take Enter as a new line, and only the mouse ran it.""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtCore import Qt +from PySide6.QtGui import QKeySequence, QShortcut +from PySide6.QtTest import QTest +from PySide6.QtWidgets import QApplication + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +def _tool(module: str, name: str, *args): + import importlib + return getattr(importlib.import_module(f"pybreeze.pybreeze_ui.tools_gui.{module}"), name)(*args) + + +# (module, class, constructor arguments, the button Ctrl+Enter presses) +TOOLS = [ + ("curl_import_gui", "CurlImportGUI", (None,), "convert_button"), + ("diff_gui", "DiffGUI", (None,), "compare_button"), + ("hash_gui", "HashGUI", (), "hash_button"), + ("header_analyzer_gui", "HeaderAnalyzerGUI", (), "analyze_button"), + ("json_format_gui", "JsonFormatGUI", (), "format_button"), + ("jwt_decoder_gui", "JwtDecoderGUI", (), "decode_button"), + ("regex_gui", "RegexGUI", (), "test_button"), + ("response_inspector_gui", "ResponseInspectorGUI", (), "analyze_button"), +] + + +def _run_shortcuts(tool) -> list[QShortcut]: + return [shortcut for shortcut in tool.findChildren(QShortcut) + if shortcut.key() in (QKeySequence("Ctrl+Return"), QKeySequence("Ctrl+Enter"))] + + +@pytest.mark.parametrize(("module", "name", "args", "button"), TOOLS, ids=[tool[1] for tool in TOOLS]) +def test_ctrl_enter_presses_the_tools_button(app, module, name, args, button): + tool = _tool(module, name, *args) + pressed: list = [] + getattr(tool, button).clicked.connect(lambda: pressed.append(True)) + + shortcuts = _run_shortcuts(tool) + assert len(shortcuts) == 2 # the main keyboard's Enter and the keypad's + assert all(shortcut.context() == Qt.ShortcutContext.WidgetWithChildrenShortcut for shortcut in shortcuts) + shortcuts[0].activated.emit() + + assert pressed == [True] + assert "Ctrl+Enter" in getattr(tool, button).toolTip() + tool.deleteLater() + + +def test_a_disabled_button_is_not_pressed(app): + # A diff still being worked out keeps Compare greyed out + tool = _tool("diff_gui", "DiffGUI", None) + pressed: list = [] + tool.compare_button.clicked.connect(lambda: pressed.append(True)) + tool.compare_button.setEnabled(False) + + _run_shortcuts(tool)[0].activated.emit() + + assert pressed == [] + tool.deleteLater() + + +def test_the_keys_work_from_the_text_box(app): + tool = _tool("hash_gui", "HashGUI") + tool.show() + tool.activateWindow() + tool.input_edit.setFocus() + tool.input_edit.setPlainText("abc") + QApplication.processEvents() + + QTest.keyClick(tool.input_edit, Qt.Key.Key_Return, Qt.KeyboardModifier.ControlModifier) + + assert "ba7816bf" in tool.output_edit.toPlainText() # SHA-256 of "abc" + tool.close() + tool.deleteLater() From b5636f986359b6b95665c08def1fc0934f520ad6 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:35:19 +0800 Subject: [PATCH 083/166] Move the Ctrl+Enter helper up to pybreeze_ui --- CLAUDE.md | 1 + architecture_explore.md | 4 ++-- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- pybreeze/pybreeze_ui/{tools_gui => }/run_shortcut.py | 0 pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/diff_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/hash_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/json_format_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/regex_gui.py | 2 +- pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py | 2 +- 13 files changed, 21 insertions(+), 11 deletions(-) rename pybreeze/pybreeze_ui/{tools_gui => }/run_shortcut.py (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 340f43ed..a44ab253 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,7 @@ pybreeze/ │ ├── exact_text.py # exact_text: a text box read as typed (toPlainText changes U+00A0, U+2028) │ ├── terminal_view.py # Terminal output in a text view: colours, the pty size, a lone \r rewinding the line │ ├── fixed_pitch.py # use_fixed_pitch_font: the system's fixed-pitch font, whatever the theme names +│ ├── run_shortcut.py # press_on_ctrl_enter: Ctrl+Enter in a panel presses its main button │ ├── error_text.py # error_text: a tool's English error (exception_tags) in the IDE language │ ├── code_result_logs.py # Only warnings and errors from loggers reach the editor's Code Result panel │ ├── closing.py # may_close / AskingDock: tabs and docks with unsaved work are asked first diff --git a/architecture_explore.md b/architecture_explore.md index 376af015..3614169d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -215,7 +215,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 6. 工具分頁 `pybreeze_ui/tools_gui/`(13 個工具 widget + 3 個共用機制) +## 6. 工具分頁 `pybreeze_ui/tools_gui/`(13 個工具 widget + 2 個共用機制) 每個工具都是 `QWidget`,UI 極薄,真正邏輯全在 `pybreeze/utils/` 對應的純函式套件裡(所以測得動、也測了)。 @@ -238,7 +238,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 兩個橫向共用機制 - **`tool_tabs.open_tool_tab()`** — 工具之間互相「轉交」:Response Inspector 把狀態碼丟給 HTTP Status、headers 丟給 Header Analyzer、JWT 丟給 JWT Decoder、JSON body 丟給 JSON Format;curl 匯入把 URL 丟給 URL Builder。開新分頁並自動聚焦。 -- **`run_shortcut.press_on_ctrl_enter()`** — 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發 +- **`pybreeze_ui/run_shortcut.press_on_ctrl_enter()`**(不在 `tools_gui/` 裡)— 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發 - **`output_actions.OutputActions`** — 統一的「複製 / 在編輯器開啟 / 存檔」三顆按鈕,綁在工具的唯讀輸出 `QTextEdit` 上,輸出也經 `exact_text()` 讀(不讓 U+00A0、U+2028 被改掉);副檔名與檔名可傳 callable 動態決定。存檔經 `replace_text()` 整檔替換,失敗(唯讀資料夾、被鎖住的檔案、磁碟滿)時原檔不動、會跳警告,說出檔名與原因。Qt 的文字元件留不住貼上的 CR,換行一律讀成 LF --- diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index d3b1852a..645cbb41 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -288,3 +288,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the Built-in Tools section of the three READMEs; `architecture_explore.md` (§6's shared mechanisms, file and test counts). - **Files**: `pybreeze/pybreeze_ui/tools_gui/run_shortcut.py`, the eight tools' `*_gui.py` in `pybreeze/pybreeze_ui/tools_gui/`, `test/test_utils/test_tool_run_shortcut.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-32 · 2026-09-25 · Refactor: the Ctrl+Enter helper moves up to pybreeze_ui · #refactor #ui + +- **What**: `press_on_ctrl_enter` (U-20260925-31) is about to serve the AI panels too (U-20260925-33), which are not tools, so it moves from `pybreeze_ui/tools_gui/run_shortcut.py` to `pybreeze_ui/run_shortcut.py`, beside `fixed_pitch.py`, another helper several kinds of panel share. +- **Change**: `git mv` and the eight tools' imports; `CLAUDE.md`'s tree lists the module; `architecture_explore.md` §6 counts two shared mechanisms in `tools_gui/` again and says where the helper lives. No behaviour changes. +- **Tests**: `test_tool_run_shortcut.py` passes (10). +- **Files**: `pybreeze/pybreeze_ui/run_shortcut.py` (moved), the eight tools' `*_gui.py`, `CLAUDE.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 84e0d2f9..7da2f0e4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-32 | 2026-09-25 | Refactor: the Ctrl+Enter helper moves up to pybreeze_ui | #refactor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-31 | 2026-09-25 | Ctrl+Enter runs a tool | #feature #tools #readme | [2026-09-c](2026-09-c.md) | | U-20260925-30 | 2026-09-25 | The SSH login form's secret field says Passphrase for a key | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | | U-20260925-29 | 2026-09-25 | Re-measure the counts the architecture map quotes | #docs | [2026-09-c](2026-09-c.md) | @@ -434,4 +435,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 31 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 32 | diff --git a/pybreeze/pybreeze_ui/tools_gui/run_shortcut.py b/pybreeze/pybreeze_ui/run_shortcut.py similarity index 100% rename from pybreeze/pybreeze_ui/tools_gui/run_shortcut.py rename to pybreeze/pybreeze_ui/run_shortcut.py diff --git a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py index f67bd5e9..f5673cb6 100644 --- a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py @@ -14,7 +14,7 @@ ) from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.header_analyzer_gui import HeaderAnalyzerGUI from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions diff --git a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py index 43a0b4fd..269ae4d3 100644 --- a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py @@ -9,7 +9,7 @@ from je_editor import language_wrapper from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.pybreeze_ui.thread_keeper import let_run_out diff --git a/pybreeze/pybreeze_ui/tools_gui/hash_gui.py b/pybreeze/pybreeze_ui/tools_gui/hash_gui.py index c4733c4a..3f41bbdf 100644 --- a/pybreeze/pybreeze_ui/tools_gui/hash_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/hash_gui.py @@ -4,7 +4,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.hash_tools.hash_text import hash_all diff --git a/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py b/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py index 6f08ca4c..b93c1b35 100644 --- a/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py @@ -12,7 +12,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.jwt_decoder_gui import JwtDecoderGUI from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions diff --git a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py index 7a0a1c43..85ce4720 100644 --- a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py @@ -6,7 +6,7 @@ ) from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.exception.exceptions import ITEJsonException diff --git a/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py b/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py index 750d9fc6..a020b9fa 100644 --- a/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py @@ -9,7 +9,7 @@ from PySide6.QtWidgets import QLabel, QPushButton, QTextEdit, QVBoxLayout, QWidget from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.exception.exceptions import JwtDecodeException diff --git a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py index e6b018b4..8f6f2965 100644 --- a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py @@ -8,7 +8,7 @@ ) from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.thread_keeper import let_run_out from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions diff --git a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py index 8705ca13..673ab5b0 100644 --- a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py @@ -11,7 +11,7 @@ ) from je_editor import language_wrapper -from pybreeze.pybreeze_ui.tools_gui.run_shortcut import press_on_ctrl_enter +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.header_analyzer_gui import HeaderAnalyzerGUI from pybreeze.pybreeze_ui.tools_gui.http_status_gui import HttpStatusGUI, status_heading From e862792f07d10a4e5a69361253c204cfd8f2dfd4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:37:05 +0800 Subject: [PATCH 084/166] Send from the AI panels with Ctrl+Enter --- README.md | 2 ++ README/README_zh-CN.md | 2 ++ README/README_zh-TW.md | 2 ++ architecture_explore.md | 4 +-- docs/updates/2026-09-c.md | 10 ++++++++ docs/updates/README.md | 3 ++- .../connect_gui/url/ai_code_review_gui.py | 2 ++ .../code_review/cot_code_review_gui.py | 2 ++ .../extend_ai_gui/skills/skills_send_gui.py | 2 ++ test/test_utils/test_tool_run_shortcut.py | 25 +++++++++++++++++++ 10 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa76460a..a8fe6b5f 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,8 @@ Password or private-key authentication (the key file picked with Browse, startin ## AI-Assisted Development +As in the tools, Ctrl+Enter in AI Code Review, CoT Code Review or Skill Send presses its send button. + ### AI Code Review ![AI code review client](images/ai_code_review.png) diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 199885a7..d032bdb0 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -163,6 +163,8 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ## AI 辅助开发 +和工具一样,在 AI 代码审查、CoT 代码审查或 Skill Send 中按 Ctrl+Enter,就等于按下发送按钮。 + ### AI 代码审查 ![AI 代码审查客户端](../images/ai_code_review.png) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index a9907d85..1f4855a7 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -163,6 +163,8 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ## AI 輔助開發 +和工具一樣,在 AI 程式碼審查、CoT 程式碼審查或 Skill Send 裡按 Ctrl+Enter,就等於按下送出鈕。 + ### AI 程式碼審查 ![AI 程式碼審查用戶端](../images/ai_code_review.png) diff --git a/architecture_explore.md b/architecture_explore.md index 3614169d..0ca64dbb 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -238,7 +238,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 兩個橫向共用機制 - **`tool_tabs.open_tool_tab()`** — 工具之間互相「轉交」:Response Inspector 把狀態碼丟給 HTTP Status、headers 丟給 Header Analyzer、JWT 丟給 JWT Decoder、JSON body 丟給 JSON Format;curl 匯入把 URL 丟給 URL Builder。開新分頁並自動聚焦。 -- **`pybreeze_ui/run_shortcut.press_on_ctrl_enter()`**(不在 `tools_gui/` 裡)— 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發 +- **`pybreeze_ui/run_shortcut.press_on_ctrl_enter()`**(不在 `tools_gui/` 裡)— 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發;AI Code Review、CoT Code Review 與 Skill Send 的送出鈕也用它 - **`output_actions.OutputActions`** — 統一的「複製 / 在編輯器開啟 / 存檔」三顆按鈕,綁在工具的唯讀輸出 `QTextEdit` 上,輸出也經 `exact_text()` 讀(不讓 U+00A0、U+2028 被改掉);副檔名與檔名可傳 callable 動態決定。存檔經 `replace_text()` 整檔替換,失敗(唯讀資料夾、被鎖住的檔案、磁碟滿)時原檔不動、會跳警告,說出檔名與原因。Qt 的文字元件留不住貼上的 CR,換行一律讀成 LF --- @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2501 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2504 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 645cbb41..9f8f8f4d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -296,3 +296,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Tests**: `test_tool_run_shortcut.py` passes (10). - **Files**: `pybreeze/pybreeze_ui/run_shortcut.py` (moved), the eight tools' `*_gui.py`, `CLAUDE.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-33 · 2026-09-25 · Ctrl+Enter sends from the AI panels · #feature #ai #readme + +- **What**: AI Code Review, CoT Code Review and Skill Send each have one send button and a multi-line code or prompt box, where Enter is a new line; sending took the mouse, where the tools now take Ctrl+Enter (U-20260925-31). +- **Change**: each panel calls `press_on_ctrl_enter(self, self.send_button)` (moved to `pybreeze_ui/run_shortcut.py` in U-20260925-32): Ctrl+Enter while the focus is in the panel presses Send, and not while a request is in flight (the button is disabled then). +- **Tests**: `test_ctrl_enter_sends_from_an_ai_panel` in `test_tool_run_shortcut.py` (3, one per panel, with the send slot disconnected so nothing goes out); all failed before the change. With the AI panel, lifecycle and closed-panel tests, 88 pass. +- **Result / numbers**: 2504 tests in 141 files. `ruff check` clean. +- **Docs**: the AI-Assisted Development section of the three READMEs; `architecture_explore.md` (the helper's users, test count). +- **Files**: `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py`, `pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py`, `test/test_utils/test_tool_run_shortcut.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7da2f0e4..9c3450d8 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-33 | 2026-09-25 | Ctrl+Enter sends from the AI panels | #feature #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-32 | 2026-09-25 | Refactor: the Ctrl+Enter helper moves up to pybreeze_ui | #refactor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-31 | 2026-09-25 | Ctrl+Enter runs a tool | #feature #tools #readme | [2026-09-c](2026-09-c.md) | | U-20260925-30 | 2026-09-25 | The SSH login form's secret field says Passphrase for a key | #fix #ssh #i18n #readme | [2026-09-c](2026-09-c.md) | @@ -435,4 +436,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 32 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 33 | diff --git a/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py b/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py index affee51a..02ea6a55 100644 --- a/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py +++ b/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py @@ -12,6 +12,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.thread_keeper import let_run_out from pybreeze.utils.app_dirs import pybreeze_data_dir from pybreeze.utils.file_process.replace_file import replace_text @@ -168,6 +169,7 @@ def __init__(self): self.send_button = QPushButton( self.word_dict.get("ai_code_review_gui_button_send_request")) self.send_button.clicked.connect(self.send_request) + press_on_ctrl_enter(self, self.send_button) main_layout.addWidget(self.send_button) main_layout.addLayout(self._build_verdict_buttons()) self.setLayout(main_layout) diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py index 1bf33e1c..41e0b848 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py @@ -4,6 +4,7 @@ QMessageBox from je_editor import language_wrapper +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.extend_ai_gui.ai_gui_global_variable import COT_TEMPLATE_FILES from pybreeze.pybreeze_ui.extend_ai_gui.code_review.code_review_thread import SenderThread from pybreeze.pybreeze_ui.thread_keeper import let_run_out @@ -67,6 +68,7 @@ def __init__(self): # 綁定事件 self.response_selector.currentTextChanged.connect(self.show_response) self.send_button.clicked.connect(self.start_sending) + press_on_ctrl_enter(self, self.send_button) # 儲存回覆 self.responses = {} diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py index 8b59ebe1..db29e720 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py @@ -10,6 +10,7 @@ from PySide6.QtCore import QThread, Signal from je_editor import language_wrapper +from pybreeze.pybreeze_ui.run_shortcut import press_on_ctrl_enter from pybreeze.pybreeze_ui.extend_ai_gui.ai_gui_global_variable import ( SKILLS_TEMPLATE_FILES, SKILLS_TEMPLATE_RELATION ) @@ -135,6 +136,7 @@ def __init__(self): # 傳送按鈕 self.send_button = QPushButton(language_wrapper.language_word_dict.get("skills_send_button")) self.send_button.clicked.connect(self.send_prompt) + press_on_ctrl_enter(self, self.send_button) layout.addWidget(self.send_button) # 回傳結果顯示區域 diff --git a/test/test_utils/test_tool_run_shortcut.py b/test/test_utils/test_tool_run_shortcut.py index 9b1867bd..f9cfb05e 100644 --- a/test/test_utils/test_tool_run_shortcut.py +++ b/test/test_utils/test_tool_run_shortcut.py @@ -86,3 +86,28 @@ def test_the_keys_work_from_the_text_box(app): assert "ba7816bf" in tool.output_edit.toPlainText() # SHA-256 of "abc" tool.close() tool.deleteLater() + + +# The AI panels have one send button each, and a code or prompt box where Enter is a new line +AI_PANELS = [ + ("pybreeze.pybreeze_ui.connect_gui.url.ai_code_review_gui", "AICodeReviewClient"), + ("pybreeze.pybreeze_ui.extend_ai_gui.code_review.cot_code_review_gui", "CoTCodeReviewGUI"), + ("pybreeze.pybreeze_ui.extend_ai_gui.skills.skills_send_gui", "SkillsSendGUI"), +] + + +@pytest.mark.parametrize(("module", "name"), AI_PANELS, ids=[panel[1] for panel in AI_PANELS]) +def test_ctrl_enter_sends_from_an_ai_panel(app, module, name): + import importlib + + panel = getattr(importlib.import_module(module), name)() + pressed: list = [] + panel.send_button.clicked.disconnect() # nothing goes out + panel.send_button.clicked.connect(lambda: pressed.append(True)) + + shortcuts = _run_shortcuts(panel) + assert len(shortcuts) == 2 + shortcuts[0].activated.emit() + + assert pressed == [True] + panel.deleteLater() From d96be79c9630a07721719ea4755cffb3b18862cc Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:39:32 +0800 Subject: [PATCH 085/166] Translate the Regex tab's title --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 9 +++++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_traditional_chinese.py | 4 ++-- test/test_utils/test_language_parity.py | 8 ++++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 0ca64dbb..8b72240b 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2504 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2505 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 9f8f8f4d..a692f9ba 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -306,3 +306,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the AI-Assisted Development section of the three READMEs; `architecture_explore.md` (the helper's users, test count). - **Files**: `pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py`, `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py`, `pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py`, `test/test_utils/test_tool_run_shortcut.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-34 · 2026-09-25 · The Regex tab's Traditional Chinese title · #fix #tools #i18n + +- **What**: in Traditional Chinese, Tools ▸ 正規表示式測試器分頁 opened a tab, and a dock, titled "Regex": the only tool tab left in English but Query / JSON, a pair of technical terms that reads the same in both languages. +- **Fix**: the tab and dock title is 正規表示式. +- **Tests**: `test_a_tab_is_titled_as_its_menu_entry_names_it` in `test_language_parity.py` (`TestLabelsAreTold`): each tool's tab title appears in its Tools menu entry, in both languages; it failed only on this one. With the regex tests, 38 pass. +- **Result / numbers**: 2505 tests in 141 files. `ruff check` clean. +- **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 9c3450d8..f96faeec 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-34 | 2026-09-25 | The Regex tab's Traditional Chinese title | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-33 | 2026-09-25 | Ctrl+Enter sends from the AI panels | #feature #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-32 | 2026-09-25 | Refactor: the Ctrl+Enter helper moves up to pybreeze_ui | #refactor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-31 | 2026-09-25 | Ctrl+Enter runs a tool | #feature #tools #readme | [2026-09-c](2026-09-c.md) | @@ -436,4 +437,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 33 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 34 | diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 8a80e0b4..a56b979e 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -511,9 +511,9 @@ "url_builder_empty_hint": "請在上方輸入 URL 或描述 URL 各部分的 JSON 物件。", # 正規表示式測試器 — 選單 "extend_tools_menu_regex_tab_action": "正規表示式測試器分頁", - "extend_tools_menu_regex_tab_label": "Regex", + "extend_tools_menu_regex_tab_label": "正規表示式", "extend_tools_menu_regex_dock_action": "正規表示式測試器停駐窗格", - "extend_tools_menu_regex_dock_title": "Regex", + "extend_tools_menu_regex_dock_title": "正規表示式", # 正規表示式測試器 — 介面 "regex_pattern_label": "樣式:", "regex_pattern_placeholder": r"(\d{4})-(\d{2})-(\d{2})", diff --git a/test/test_utils/test_language_parity.py b/test/test_utils/test_language_parity.py index d217a77c..6af4ab7d 100644 --- a/test/test_utils/test_language_parity.py +++ b/test/test_utils/test_language_parity.py @@ -126,6 +126,14 @@ def test_every_tools_entry_says_whether_it_opens_a_tab_or_a_dock(self): for *_start, action_key in _DOCK_ACTIONS: assert EN[action_key].endswith(" Dock") and ZH[action_key].endswith("停駐窗格"), action_key + def test_a_tab_is_titled_as_its_menu_entry_names_it(self): + # The Regex tab was "Regex" in Traditional Chinese, opened by 正規表示式測試器分頁 + from pybreeze.pybreeze_ui.menu.tools.tools_menu import _TAB_ACTIONS + + for *_start, action_key, label_key in _TAB_ACTIONS: + for words in (EN, ZH): + assert words[label_key] in words[action_key], (label_key, words[label_key], words[action_key]) + def test_a_dock_is_titled_like_the_tab_of_the_same_tool(self): # "AI Code-Review" and "Skill Send GUI" as titles; the Skill Send dock's # menu entry read "Skill Prompt Dock" From d14808e17bbc22d1c001d1028e767d2dfc49fbcc Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:45:23 +0800 Subject: [PATCH 086/166] Rename and delete in the project tree with F2 and Delete --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 +- docs/updates/2026-09-c.md | 10 ++++ docs/updates/README.md | 3 +- .../editor_main/file_tree_context_menu.py | 32 ++++++++++ .../test_utils/test_file_tree_context_menu.py | 59 +++++++++++++++++++ 8 files changed, 108 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a8fe6b5f..dc217549 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Password or private-key authentication (the key file picked with Browse, startin ### And also -- **File Tree Context Menu** — right-click to create, rename, delete, copy absolute or relative paths, or reveal the item in your platform file manager (a file shown selected in Explorer and Finder). Renaming or deleting a file open in an editor tab keeps the tab in sync. +- **File Tree Context Menu** — right-click to create, rename, delete, copy absolute or relative paths, or reveal the item in your platform file manager (a file shown selected in Explorer and Finder). F2 renames and Delete deletes the item in focus while the tree has the focus; Delete asks first, with No as the default. Renaming or deleting a file open in an editor tab keeps the tab in sync. - **Package Manager** — install automation modules and build tools from the menu, output in a run window. - **Integrated Documentation** — each module's docs and GitHub page open as in-IDE browser tabs (TestPioneer's documentation is its GitHub README). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index d032bdb0..b38ce7d7 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -155,7 +155,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ### 其他 -- **文件树右键菜单** — 右键即可创建、重命名、删除、复制绝对或相对路径,或在系统的文件管理器中显示该项目(在 Explorer 与 Finder 中会选中该文件)。重命名或删除一个已在编辑器标签页中打开的文件时,标签页会保持同步。 +- **文件树右键菜单** — 右键即可创建、重命名、删除、复制绝对或相对路径,或在系统的文件管理器中显示该项目(在 Explorer 与 Finder 中会选中该文件)。焦点在文件树时,F2 重命名、Delete 删除当前项目;Delete 会先询问,默认是“否”。重命名或删除一个已在编辑器标签页中打开的文件时,标签页会保持同步。 - **包管理器** — 从菜单安装自动化模块与构建工具,输出显示在运行窗口中。 - **集成文档** — 每个模块的文档与 GitHub 页面都以 IDE 内的浏览器标签页打开(TestPioneer 的文档就是它 GitHub 上的 README)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 1f4855a7..9b12fc30 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -155,7 +155,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ### 其他 -- **檔案樹右鍵選單** — 按右鍵即可建立、重新命名、刪除、複製絕對或相對路徑,或在系統的檔案管理員中顯示該項目(在 Explorer 與 Finder 中會選取該檔案)。重新命名或刪除已在編輯器分頁中開啟的檔案時,分頁會同步更新。 +- **檔案樹右鍵選單** — 按右鍵即可建立、重新命名、刪除、複製絕對或相對路徑,或在系統的檔案管理員中顯示該項目(在 Explorer 與 Finder 中會選取該檔案)。焦點在檔案樹時,F2 重新命名、Delete 刪除目前的項目;Delete 會先詢問,預設是「否」。重新命名或刪除已在編輯器分頁中開啟的檔案時,分頁會同步更新。 - **套件管理員** — 從選單安裝自動化模組與建置工具,輸出顯示在執行視窗中。 - **整合文件** — 每個模組的文件與 GitHub 頁面都以 IDE 內的瀏覽器分頁開啟(TestPioneer 的文件就是它 GitHub 上的 README)。 diff --git a/architecture_explore.md b/architecture_explore.md index 8b72240b..745ad045 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -74,7 +74,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE - `add_menu_to_menubar()` — 建構全部選單(見 §5) - `syntax_extend_package()` — 註冊 `.json` / `.yml` / `.yaml` 自動化關鍵字高亮 - 依 `EDITOR_EXTEND_TAB` 註冊表加入外部擴充分頁(`_add_extend_tabs()`:每一個分頁各自建,建不起來的只記 log,不會讓整個 IDE 起不來) - - `setup_file_tree_context_menu()` — 掛上檔案樹右鍵選單。改名時開著的分頁跟著檔案走(改資料夾也一樣,底下每個開著的檔案都跟著走):先停掉分頁的自動存檔、改名、再用新路徑重開一條(`_stop_auto_save()` / `_start_auto_save()`)——JEditor 的存檔執行緒只認開檔當下的路徑,沒辦法改指向。外部修改監視也跟著搬(改名前就先移除,檔案搬走後 Windows 放不掉舊名),並照 `open_an_file` 重載語法高亮、git 基準與語言伺服器(`rename_self_tab()` 會清掉「未儲存」標記,有未存的編輯就用 `_on_text_changed()` 放回去,否則改名後五秒內關分頁會直接丟掉編輯);Dock Editor(`FullEditorWidget`,關閉時才寫回、檔案不存在就不寫)的 `current_file` 也改指新路徑(`_dock_editors_under()`)。新增與改名的名稱不能帶磁碟代號、根目錄、`..` 或 `:`,也不能解析到資料夾外(`_inside()`;改名只能是單一名稱)。刪除資料夾用 `remove_folder()`(唯讀檔清掉唯讀屬性再刪,git 的物件檔就是唯讀),符號連結與 junction 只刪連結本身。刪除時同樣用 `_editors_under()`:檔案或資料夾底下每個開著的分頁先停掉自動存檔,再刪;刪完只關掉檔案真的不見了的分頁,刪不掉(被鎖住、唯讀)的檔案分頁留著、自動存檔重開。「在檔案總管中顯示」由 `reveal_command()` 組指令:Windows 用 Explorer 的 `/select,`、macOS 用 `open -R` 把檔案選起來,其他平台 `xdg-open` 只能開資料夾;啟動失敗(例如沒有 `xdg-open`)經 `_perform_file_op()` 跳警告 + - `setup_file_tree_context_menu()` — 掛上檔案樹右鍵選單,以及焦點在樹上時的 F2(重新命名)與 Delete(刪除,先問、預設否)快捷鍵(`_attach_keys()`,`WidgetShortcut`,走選單的同一組動作)。改名時開著的分頁跟著檔案走(改資料夾也一樣,底下每個開著的檔案都跟著走):先停掉分頁的自動存檔、改名、再用新路徑重開一條(`_stop_auto_save()` / `_start_auto_save()`)——JEditor 的存檔執行緒只認開檔當下的路徑,沒辦法改指向。外部修改監視也跟著搬(改名前就先移除,檔案搬走後 Windows 放不掉舊名),並照 `open_an_file` 重載語法高亮、git 基準與語言伺服器(`rename_self_tab()` 會清掉「未儲存」標記,有未存的編輯就用 `_on_text_changed()` 放回去,否則改名後五秒內關分頁會直接丟掉編輯);Dock Editor(`FullEditorWidget`,關閉時才寫回、檔案不存在就不寫)的 `current_file` 也改指新路徑(`_dock_editors_under()`)。新增與改名的名稱不能帶磁碟代號、根目錄、`..` 或 `:`,也不能解析到資料夾外(`_inside()`;改名只能是單一名稱)。刪除資料夾用 `remove_folder()`(唯讀檔清掉唯讀屬性再刪,git 的物件檔就是唯讀),符號連結與 junction 只刪連結本身。刪除時同樣用 `_editors_under()`:檔案或資料夾底下每個開著的分頁先停掉自動存檔,再刪;刪完只關掉檔案真的不見了的分頁,刪不掉(被鎖住、唯讀)的檔案分頁留著、自動存檔重開。「在檔案總管中顯示」由 `reveal_command()` 組指令:Windows 用 Explorer 的 `/select,`、macOS 用 `open -R` 把檔案選起來,其他平台 `xdg-open` 只能開資料夾;啟動失敗(例如沒有 `xdg-open`)經 `_perform_file_op()` 跳警告 - `close_tab()` 覆寫 JEditor 的:分頁有 `may_close()` 就先問(提示詞編輯器、架構圖編輯器有未存的變更時會問);關掉的工具分頁 `deleteLater()`(JEditor 的 `removeTab` 不刪 widget,關過的工具分頁會留到 IDE 結束),JEditor 自己的編輯器分頁不動,關閉 IDE 時也先問過每個分頁與 dock,有一個說不就取消關閉 - `debug_mode=True` 時啟動 10 秒自動關閉 `QTimer`(CI 用) 3. `apply_stylesheet()` 套 qt_material 主題(預設 `dark_amber.xml`) @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2505 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2510 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index a692f9ba..4ffbb25b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -315,3 +315,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2505 tests in 141 files. `ruff check` clean. - **Files**: `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_language_parity.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-35 · 2026-09-25 · F2 and Delete in the project tree · #feature #ui #readme + +- **What**: the project tree renamed and deleted only through its right-click menu; F2 and Delete, the keys a file manager uses, did nothing, and JEditor's tree handles neither. +- **Change**: `_attach_keys` (`editor_main/file_tree_context_menu.py`), run when the menu is attached, gives the tree F2 (rename) and Delete (delete) shortcuts that act on the entry in focus through the menu's own `_action_rename` and `_action_delete`. They are `WidgetShortcut`s: only while the tree itself has the focus, so Delete in the editor beside it stays the editor's. Delete asks first, No being the default (U-20260925-23). The menu's Rename and Delete entries show the keys. +- **Tests**: new `TestTheKeys` in `test_file_tree_context_menu.py` (5): each key has one tree-scoped shortcut acting on the current entry, a real F2 or Delete typed in a shown tree reaches the action (the view's own key handling, which starts an edit on F2, does not take it), and attaching twice adds the keys once. The first four failed before the change. With the context menu and rename tests, 67 pass. +- **Result / numbers**: 2510 tests in 141 files. `ruff check` clean; `C901` clean. +- **Docs**: the File Tree Context Menu line of the three READMEs; `architecture_explore.md`. +- **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `test/test_utils/test_file_tree_context_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f96faeec..2485b176 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-35 | 2026-09-25 | F2 and Delete in the project tree | #feature #ui #readme | [2026-09-c](2026-09-c.md) | | U-20260925-34 | 2026-09-25 | The Regex tab's Traditional Chinese title | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-33 | 2026-09-25 | Ctrl+Enter sends from the AI panels | #feature #ai #readme | [2026-09-c](2026-09-c.md) | | U-20260925-32 | 2026-09-25 | Refactor: the Ctrl+Enter helper moves up to pybreeze_ui | #refactor #ui | [2026-09-c](2026-09-c.md) | @@ -437,4 +438,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 34 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 35 | diff --git a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py index 1f85adff..aea7a7dc 100644 --- a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py +++ b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py @@ -9,6 +9,7 @@ from pathlib import Path, PureWindowsPath from PySide6.QtCore import Qt, QModelIndex +from PySide6.QtGui import QKeySequence, QShortcut from PySide6.QtWidgets import ( QTreeView, QMenu, QFileSystemModel, QInputDialog, QMessageBox, QApplication, @@ -72,6 +73,34 @@ def _attach_context_menu(tree_view: QTreeView, main_window) -> None: tree_view.customContextMenuRequested.connect( lambda pos, tv=tree_view, mw=main_window: _show_context_menu(pos, tv, mw) ) + _attach_keys(tree_view, main_window) + + +# Keys that act on the entry in focus while the tree has the focus, as in a file manager +_RENAME_KEY = QKeySequence(Qt.Key.Key_F2) +_DELETE_KEY = QKeySequence(Qt.Key.Key_Delete) + + +def _attach_keys(tree_view: QTreeView, main_window) -> None: + """F2 renames and Delete deletes the current entry, through the menu's own actions. + + Only while the tree itself has the focus (``WidgetShortcut``): Delete in the + editor beside it is the editor's. Delete asks first, No being the default. + """ + for keys, act in ((_RENAME_KEY, "rename"), (_DELETE_KEY, "delete")): + shortcut = QShortcut(keys, tree_view) + shortcut.setContext(Qt.ShortcutContext.WidgetShortcut) + shortcut.activated.connect( + lambda tv=tree_view, mw=main_window, what=act: _act_on_current(what, tv, mw)) + + +def _act_on_current(what: str, tree_view: QTreeView, main_window) -> None: + """Rename or delete (*what*) the entry in focus, if there is one.""" + path = _get_path_from_index(tree_view, tree_view.currentIndex()) + if what == "rename": + _action_rename(tree_view, main_window, path) + else: + _action_delete(tree_view, main_window, path) def _get_path_from_index(tree_view: QTreeView, index: QModelIndex) -> Path | None: @@ -107,6 +136,9 @@ def _show_context_menu(pos, tree_view: QTreeView, main_window) -> None: delete_act = menu.addAction(word.get("file_tree_ctx_delete")) rename_act.setEnabled(path is not None) delete_act.setEnabled(path is not None) + for act, keys in ((rename_act, _RENAME_KEY), (delete_act, _DELETE_KEY)): + act.setShortcut(keys) # shown beside the entry: the tree's own keys do it + act.setShortcutVisibleInContextMenu(True) menu.addSeparator() # --- Clipboard --- diff --git a/test/test_utils/test_file_tree_context_menu.py b/test/test_utils/test_file_tree_context_menu.py index 5de4c68a..80293240 100644 --- a/test/test_utils/test_file_tree_context_menu.py +++ b/test/test_utils/test_file_tree_context_menu.py @@ -7,6 +7,7 @@ from __future__ import annotations import os +import time from pathlib import Path os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") @@ -435,6 +436,64 @@ def test_setup_leaves_later_tabs_working(self, app): placeholder.deleteLater() +class TestTheKeys: + """F2 renames and Delete deletes the entry in focus, as in a file manager: only the menu did.""" + + @staticmethod + def _shortcut(tree, keys: str): + from PySide6.QtGui import QKeySequence, QShortcut + + found = [shortcut for shortcut in tree.findChildren(QShortcut) if shortcut.key() == QKeySequence(keys)] + assert len(found) == 1, keys + assert found[0].context() == Qt.ShortcutContext.WidgetShortcut # only while the tree has the focus + return found[0] + + @pytest.mark.parametrize(("keys", "action"), [("F2", "_action_rename"), ("Del", "_action_delete")]) + def test_the_key_acts_on_the_current_entry(self, tree, tmp_path, monkeypatch, keys, action): + target = tmp_path / "notes.py" + target.touch() + asked: list = [] + monkeypatch.setattr(ctx, action, lambda _tree, _window, path: asked.append(path)) + _attach_context_menu(tree, FakeWindow()) + tree.setCurrentIndex(tree.model().index(str(target))) + + self._shortcut(tree, keys).activated.emit() + + assert asked == [target] + + @pytest.mark.parametrize(("key", "action"), [(Qt.Key.Key_F2, "_action_rename"), + (Qt.Key.Key_Delete, "_action_delete")]) + def test_a_key_pressed_in_the_tree_reaches_it(self, tree, tmp_path, monkeypatch, key, action): + # The view handles keys of its own (F2 starts an edit): the shortcut must still get them + from PySide6.QtTest import QTest + + target = tmp_path / "notes.py" + target.touch() + asked: list = [] + monkeypatch.setattr(ctx, action, lambda _tree, _window, path: asked.append(path)) + _attach_context_menu(tree, FakeWindow()) + tree.show() + tree.activateWindow() + tree.setFocus() + # The model lists the folder in the background, as it does for a user who then picks a file + deadline = time.monotonic() + 10 + while tree.model().rowCount(tree.rootIndex()) == 0 and time.monotonic() < deadline: + QApplication.processEvents() + tree.setCurrentIndex(tree.model().index(str(target))) + QApplication.processEvents() + + QTest.keyClick(tree, key) + + assert asked == [target] + tree.close() + + def test_attaching_twice_adds_the_keys_once(self, tree): + window = FakeWindow() + _attach_context_menu(tree, window) + _attach_context_menu(tree, window) + self._shortcut(tree, "F2") + + class TestANameStaysInItsFolder: """A drive, a root, '..' or ':' put the file elsewhere: /tmp/notes.py became C:\\tmp\\notes.py.""" From f971f46c945da7d0ea8262731ce3fa9ab9be7286 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:46:38 +0800 Subject: [PATCH 087/166] Pull the SFTP tree's action runner out of its menu --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- .../connect_gui/ssh/ssh_file_viewer_widget.py | 19 +++++++++++++------ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 4ffbb25b..c5ed199b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -325,3 +325,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the File Tree Context Menu line of the three READMEs; `architecture_explore.md`. - **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `test/test_utils/test_file_tree_context_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-36 · 2026-09-25 · Refactor: the SFTP tree's menu action runner and entry lookup · #refactor #ssh + +- **What**: the SFTP tree's keys (U-20260925-37) need what `on_context_menu` did inline: find the entry a row stands for (the "..." or loading row has none, so its folder) and run an action on it, reporting a failed SFTP request instead of raising it. +- **Change**: `SSHFileTreeManager._entry_of(item)` and `_act_on(handler, item)` (`connect_gui/ssh/ssh_file_viewer_widget.py`); `on_context_menu` uses both. No behaviour changes. +- **Tests**: the SFTP tree action and cancel tests, the menu-freeing and closed-panel tests pass (63). The full suite at `d96be79` passed (2490 passed, 15 skipped), and both startup tests exit 0. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 2485b176..416f27d6 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-36 | 2026-09-25 | Refactor: the SFTP tree's menu action runner and entry lookup | #refactor #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-35 | 2026-09-25 | F2 and Delete in the project tree | #feature #ui #readme | [2026-09-c](2026-09-c.md) | | U-20260925-34 | 2026-09-25 | The Regex tab's Traditional Chinese title | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-33 | 2026-09-25 | Ctrl+Enter sends from the AI panels | #feature #ai #readme | [2026-09-c](2026-09-c.md) | @@ -438,4 +439,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 35 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 36 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index 8dc6e162..5b8855a6 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -394,10 +394,7 @@ def on_context_menu(self, pos): Show context menu for file operations. 顯示右鍵選單以進行檔案操作。 """ - item = self.tree.itemAt(pos) - if item is not None and not item.text(3): - # The "..." or loading row: no entry of its own, so the folder it is in - item = item.parent() + item = self._entry_of(self.tree.itemAt(pos)) menu = QMenu(self) handlers = {} for name, handler in ( @@ -417,8 +414,18 @@ def on_context_menu(self, pos): chosen = menu.exec(self.tree.viewport().mapToGlobal(pos)) menu.deleteLater() # a child of this widget: kept for good otherwise, one per right-click handler = handlers.get(chosen) - if handler is None: - return + if handler is not None: + self._act_on(handler, item) + + @staticmethod + def _entry_of(item: QTreeWidgetItem | None) -> QTreeWidgetItem | None: + """The entry *item* stands for: the "..." or loading row has none of its own, so its folder.""" + if item is not None and not item.text(3): + return item.parent() + return item + + def _act_on(self, handler: Callable[[QTreeWidgetItem | None], None], item: QTreeWidgetItem | None) -> None: + """Run a tree action on *item*; a failed SFTP request is reported, not raised.""" try: handler(item) # what an SFTP operation raises, a closed session's RuntimeError included From 731978e259e33e862a31c24177906ab33039feb8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:49:37 +0800 Subject: [PATCH 088/166] Rename and delete in the SFTP tree with F2 and Delete --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 8 ++-- docs/updates/2026-09-c.md | 10 +++++ docs/updates/README.md | 3 +- .../connect_gui/ssh/ssh_file_viewer_widget.py | 24 ++++++++++- test/test_utils/test_sftp_tree_actions.py | 41 +++++++++++++++++++ 8 files changed, 83 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index dc217549..82a9f0ac 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ A WYSIWYG `QGraphicsScene` editor: rectangle, rounded, ellipse and diamond nodes ![SSH client](images/ssh_client.png) -Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included; a PuTTY `.ppk` key is exported from PuTTYgen as an OpenSSH key first, as the error message says; with key authentication the password field reads Passphrase and takes the key's passphrase), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download. Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. +Password or private-key authentication (the key file picked with Browse, starting in `~/.ssh`: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, PKCS#8 included; a PuTTY `.ppk` key is exported from PuTTYgen as an OpenSSH key first, as the error message says; with key authentication the password field reads Passphrase and takes the key's passphrase), an interactive shell with keepalive that shows ANSI colours, in a fixed-pitch font, whose width and height the shell is told as the view is resized (Up and Down bring back earlier commands, Enter on an empty line reaches the shell, `clear` and `reset` wipe the view, and **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in it; the view shows output line by line, so programs that draw on the whole screen by moving the cursor, such as `vim` or `htop`, come out garbled), and a lazy-loading SFTP tree with create-folder / rename / delete / upload / download (F2 renames and Delete deletes the entry in focus, as in the project tree). Every SFTP request runs in the background, so a stalled link never freezes the IDE. An upload asks before it replaces a file on the server, and a transfer can be cancelled from the tree's menu. Both directions write to a temporary file first, so a dropped link leaves the old copy whole. Unknown host keys are **not** auto-accepted: the SHA256 fingerprint is shown for confirmation on first connection (trust on first use) and persisted to `~/.pybreeze/ssh_known_hosts`. ### And also diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index b38ce7d7..442995d2 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -151,7 +151,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ![SSH 客户端](../images/ssh_client.png) -支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以;PuTTY 的 `.ppk` 密钥需先在 PuTTYgen 导出为 OpenSSH 密钥,错误信息会说明如何操作;勾选密钥认证时,密码栏会改为“密语”,填入私钥的密语),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 +支持密码或私钥认证(用 Browse 选择密钥文件,从 `~/.ssh` 开始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 密钥,PKCS#8 也可以;PuTTY 的 `.ppk` 密钥需先在 PuTTYgen 导出为 OpenSSH 密钥,错误信息会说明如何操作;勾选密钥认证时,密码栏会改为“密语”,填入私钥的密语),带 keepalive、会显示 ANSI 颜色的交互式 shell,以等宽字体显示,窗口大小改变时会把新的宽度和高度告诉 shell(上下方向键调出之前发送的命令,空行按 Enter 也会发送到 shell,`clear` 与 `reset` 会清空画面,**Interrupt** 按钮、或在未选中文字的命令行中按 Ctrl+C,可停止 shell 中正在运行的程序;界面逐行显示输出,所以 `vim`、`htop` 这类移动光标绘制整个屏幕的程序会显示错乱),以及按需加载的 SFTP 文件树,支持创建文件夹/重命名/删除/上传/下载(和项目文件树一样,F2 重命名、Delete 删除当前项目)。每个 SFTP 请求都在后台运行,因此连接卡住时也不会冻结 IDE。上传前若会覆盖服务器上的文件会先询问,传输也可以从文件树的菜单中取消。两个方向都先写入临时文件,因此连接中断时,旧的副本仍完整保留。未知的主机密钥**不会**被自动接受:首次连接时会显示 SHA256 指纹请你确认(首次使用即信任),并保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 9b12fc30..7bd3b12c 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -151,7 +151,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ![SSH 用戶端](../images/ssh_client.png) -支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以;PuTTY 的 `.ppk` 金鑰要先在 PuTTYgen 匯出成 OpenSSH 金鑰,錯誤訊息會說明怎麼做;勾選金鑰驗證時,密碼欄會改成「密語」,填入私鑰的密語),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 +支援密碼或私鑰驗證(金鑰檔以 Browse 挑選,從 `~/.ssh` 開始:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以;PuTTY 的 `.ppk` 金鑰要先在 PuTTYgen 匯出成 OpenSSH 金鑰,錯誤訊息會說明怎麼做;勾選金鑰驗證時,密碼欄會改成「密語」,填入私鑰的密語),具備 keepalive、會顯示 ANSI 顏色的互動式 shell,以等寬字型顯示,視窗大小改變時會把新的寬度與高度告訴 shell(上下方向鍵叫回先前送出的指令,空白的一行按 Enter 也會送到 shell,`clear` 與 `reset` 會清空畫面,**Interrupt**(中斷)按鈕、或在沒有選取文字的指令列按 Ctrl+C,可停止 shell 中正在執行的程式;畫面是一行一行顯示輸出,所以 `vim`、`htop` 這類移動游標畫滿整個畫面的程式會顯示錯亂),以及延遲載入的 SFTP 檔案樹,可建立資料夾/重新命名/刪除/上傳/下載(和專案檔案樹一樣,F2 重新命名、Delete 刪除目前的項目)。每個 SFTP 請求都在背景執行,連線卡住也不會讓 IDE 凍結。上傳時若要取代伺服器上的檔案會先詢問,傳輸也能從檔案樹的選單取消。上下傳都會先寫入暫存檔,所以連線中斷時舊的檔案仍完整無缺。未知的主機金鑰**不會**自動接受:第一次連線時會顯示 SHA256 指紋供確認(首次使用即信任),並保存到 `~/.pybreeze/ssh_known_hosts`。 ### 其他 diff --git a/architecture_explore.md b/architecture_explore.md index 745ad045..ee38bab1 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -247,7 +247,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) | 檔案 | 職責 | |---|---| -| `diagram_editor_widget.py` (679) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | +| `diagram_editor_widget.py` (695) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | | `diagram_scene.py` (915) | `DiagramScene(QGraphicsScene)`:**State pattern** 的 `ToolMode` 決定滑鼠行為;undo/redo、複製貼上(節點、連線與圖片)、多選對齊與分佈、z-order、序列化 `to_dict()` / `load_from_dict()`。`get_all_nodes/connections/images()` 由下往上列出(`_bottom_first()`),存檔與 undo 還原時同 z 值的重疊項目維持原本的上下;右鍵選單先選取點到的項目;置頂/置底放到所有其他節點與圖片之上/之下;`to_dict()` 給每個節點與圖片記下它在兩者之間由下往上的位置(`stack`),載入後 `_restore_stacking()` 照這個順序重新加回場景(同 z 值時後加的在上面),圖片也存 `z`。`load_from_dict()` 先用 `_check_is_a_diagram()` 確認資料形狀才清空畫布(不合就丟 `ValueError`,畫布原封不動),每一筆節點/連線/圖片再各自容錯;清空前先 `to_dict()` 留一份,載入途中還是出錯就放回原樣再往上丟——載入要嘛成功、要嘛什麼都沒變(編輯器存檔寫回上次開的檔案,半途清空的畫布會蓋掉使用者的檔)。圖片的 `source` 不是字串就丟掉。`undo_scope` 用 `try/finally`,本體丟例外也一定收掉快照;圖片來源先看副檔名、拒絕 UNC(`_is_on_this_machine()`)才碰檔案系統;URL 圖片交給 `ImageDownloadThread(QThread)` 下載並快取在 `_pixmap_cache`,undo/redo 重建項目時直接用快取,不會再連一次網路;編輯器關閉時 `let_image_downloads_run_out()` 把還在跑的下載交給 `let_run_out()`,不在 UI 執行緒等 | | `diagram_items.py` (965) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | | `diagram_mermaid_parser.py` (613) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | @@ -298,14 +298,14 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 9. `pybreeze_ui/connect_gui/` -### `ssh/`(2,467 行) +### `ssh/`(2,508 行) | 檔案 | 職責 | |---|---| | `ssh_main_widget.py` | 組合視圖:上方共用登入表單,下方 splitter 左 30% 檔案樹、右 70% 終端。兩半各自連線、各自發 `state_changed`,共用的狀態列每次有一半連上或斷開就重報兩者的狀態(不是按下按鈕時)。`closeEvent` 把關閉往下傳給兩半(Qt 只會送給被關的那個 widget) | | `ssh_login_widget.py` | 登入表單(密碼欄用 `EchoMode.Password`;任一欄按 Enter 就按下連線;金鑰欄旁的「瀏覽...」從 `~/.ssh` 開檔案對話框,選了檔就勾起金鑰驗證) | | `ssh_command_widget.py` | 互動式 shell。連線和開 shell 的 channel(`open_shell_channel()`:開 session 最多等 paramiko 的 `channel_timeout` 一小時,pty 與 shell 沒有逾時;pty 開成 terminal 看得到的大小,`terminal_view.terminal_size()`:以等寬字型算出完整看得到的欄與列,至少 20 × 5)都在 `SshConnectThread` 上做,UI 執行緒只接手啟動 reader;連線中再按 Connect 不理,連線中關掉 widget 時執行緒交給 `let_run_out()`,晚到的連線一結束就關掉。`SSHReaderThread(QThread)` 輪詢 channel(shell 結束時先把緩衝裡剩下的讀完;伺服器那端結束 shell 時 `_on_closed()` 一樣 `_cleanup()` 關掉連線並發 `state_changed`,共用狀態列照兩半的實際狀態重報),`TerminalDecoder` 把每次讀到的 bytes 轉成 `TerminalOutput`:是否先清畫面(`clear`、`reset`:`split_at_screen_clear()`,只留最後一次清除之後的文字,清除前設的顏色延續、`ESC c` 則重設;widget 先 `clear()` 畫面並忘掉待套用的 `\r`),以及一段段帶樣式的文字(SGR 顏色與粗體等由 `utils/terminal_style.split_styled()` 讀出,跨次讀取延續、`reset()` 清掉;畫面用 `terminal_view.style_format()` 轉成 `QTextCharFormat`,一段結尾的單獨 `\r` 記在 `_rewind_pending` 給下一段套用):UTF-8 字元、escape 與結尾的 `\r` 被讀取切斷時留到下一次接上(`split_unfinished_end()`,`\r\n` 被切開不會多一行空行),escape(CSI、OSC/DCS/SOS/PM/APC 控制字串、`ESC ( B` 這類 nF、其他兩位元組 escape)用 regex 剝除,backspace 套用到前一個字元、其餘 C0 控制字元丟掉(`utils/terminal_text.py` 的 `strip_terminal_controls()`;切斷的 escape 由 `split_incomplete_escape()` 留到下一次);送出指令走 `send_all()`:整串 UTF-8 bytes 用 `sendall` 送完(`Channel.send` 一次只送一個封包),只在這次送出時給 channel 5 秒逾時;空白的一行也送出(只送換行,用來接受提示的預設值);沒連線時,空白的一行不跳「尚未連線」提示;「中斷」按鈕與指令列的 Ctrl+C(沒有選取文字時;有選取就照常複製,`eventFilter`)送出 `\x03`(`send_interrupt()`),停止 shell 中正在跑的程式,指令列打到一半的字留著;上下鍵走過送出過的指令(`CommandHistory`:最多 500 行,連續重複只記一次,往下走過最新一行就還回原本打到一半的字);輸出接在最後一行後面(不用 `appendPlainText`,那會讓每次讀取都另起一行),單獨的 `\r` 回到行首、由後面的文字取代這一行(進度條;與執行視窗共用 `terminal_view.insert_rewinding()`),輸出用等寬字型(`fixed_pitch.use_fixed_pitch_font()`,同執行視窗,欄位對齊的輸出才對得齊),terminal 的 viewport 改變大小、換算成字元數有變時送 `resize_pty`(`_follow_view_size()`,經 `eventFilter`;shell 開好時再對一次,連線中改了大小也跟上;伺服器拒絕時記 log,下次再試),自己的提示訊息才另起一行,terminal 有 block 上限,keepalive。`closeEvent` 一律 `_cleanup()`:執行緒不能活得比 widget 久(QThread 還在跑就被銷毀會讓 Qt abort) | -| `ssh_file_viewer_widget.py` (725) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | +| `ssh_file_viewer_widget.py` (756) + `sftp_session.py` (473) | `SSHFileTreeManager`(樹與右鍵選單,只看執行緒的 signal 做事;焦點在樹上時 F2 重新命名、Delete 刪除目前項目,走選單同一組動作,`_act_on()` 把斷線的錯誤變成對話框)+ `sftp_session.py` 的 `SFTPClientWrapper`、`SftpListThread` / `SftpTransferThread` / `SftpCallThread` 與純路徑工具(`remote_join`、`plain_remote_name`、`sort_entries`):延遲載入的遠端檔案樹、右鍵選單(重新整理/建資料夾/改名/刪除/下載/上傳;新名稱只能是單一項目,`plain_remote_name()` 擋掉 `/`、`.`、`..`;改名已載入的資料夾會用新路徑重列子項;從檔案項目建資料夾或上傳時重新整理它所在的資料夾,`folder_item()`)、目錄優先 + 自然排序(項目是資料夾還是檔案記在 `KIND_ROLE`,`is_folder()` / `is_file()` 讀它,不看類型欄的字);列目錄時符號連結用 `stat` 換成它指向的東西的類型與大小(`_follow_link`,伺服器列目錄用的是 `lstat`,連到資料夾的連結原本當成檔案),指不到東西的連結照舊當檔案。每個 SFTP 操作都經 `_session()`:拿 `_in_use` 鎖(paramiko 的 SFTP client 會把別的執行緒的回覆讀走丟掉,送出那個請求的執行緒就永遠等下去)、再 `_require_connection()`;列目錄與傳輸在工作執行緒上一直等,右鍵選單的建資料夾/改名/刪除交給 `SftpCallThread`(SFTP 回覆沒有逾時,放在 UI 執行緒會凍到 TCP 放棄),等 session 最多 `UI_WAIT_SECONDS`(1 秒),等不到就丟 `SftpBusy`(「連線忙碌」),完成後才更新樹(樹被清掉就不動);下載先寫到同資料夾的暫存檔(`.<檔名>.*.part`),完整了才 `os.replace` 換上,失敗就刪掉暫存檔、原檔不動;上傳同理:先 `stat` 目標,已存在又沒說要取代就什麼都不傳、發 `exists` 讓樹先問(預設「否」),再 `put` 到 `.<檔名>.<亂數>.part`,完整了才 `posix_rename`(伺服器沒有這個擴充就先刪再 `rename`)換上;連線交給 `SshConnectThread`,成功後才列出根目錄;`SFTPClientWrapper.connect()` 用區域變數建連線,登入完如果 wrapper 已經被 `close()`(Disconnect 或關分頁)就自己關掉這條連線、丟 `ConnectAbandoned`,失敗時也只關自己的;連線中按 Disconnect 會把連線執行緒交給 `let_run_out()`(不跳「連線失敗」),可以再按 Connect;每次列目錄(根目錄、展開、重新整理)交給 `SftpListThread`,先顯示「載入中」,結果回來時只在樹沒被清掉(`_tree_generation`)、而且該項目等的還是這一次(`LISTING_ROLE` 序號,重新整理會取代前一次)時才填進去;下載/上傳交給 `SftpTransferThread(QThread)`(傳輸沒有自己的逾時,跑在 UI 執行緒會把整個 IDE 凍到傳完),一次只允許一個,傳輸中右鍵選單多一項「取消傳輸」(`SftpTransferThread.cancel()`:paramiko `get` / `put` 的進度回呼每塊都檢查,丟 `TransferCancelled`,暫存檔照失敗處理刪掉、要被取代的檔案不動,發 `cancelled`);傳輸中按 Connect 不重連檔案樹(連線一開始就 `close()`,會把傳輸砍斷),只提示正在傳輸,`_refused_while_transferring()`;`closeEvent` 不等它也不打斷它(打斷會留下半個檔案),交給 `let_run_out()`,傳完才關掉 SFTP 連線 | | `ssh_host_key_policy.py` | **`InteractiveHostKeyPolicy`** — 取代 `AutoAddPolicy`。首次連線顯示 SHA256 指紋要使用者確認,確認後寫入 `~/.pybreeze/ssh_known_hosts`(TOFU)。查詢、詢問、寫入都在模組層的 `_DECISION_LOCK` 裡一次一個(只有連線執行緒會拿,UI 執行緒不等它);問之前先重讀檔案(同一次 Connect 的另一半剛接受過就不再問),使用者拒絕的 (host, 指紋) 記 10 秒,另一半直接拒絕;寫入時在檔案現況後面加一行(`_store()`),不用 `client.save_host_keys()`(那會用 Connect 時讀到的舊副本蓋掉別的分頁剛接受的主機),也不用 `HostKeys.save()`(paramiko 讀不懂的行,壞行或 `ssh-dss`,會被寫掉)。兩個 known_hosts 都經 `load_known_hosts()` 逐行讀,讀不懂的行跳過(`HostKeys.load` 遇到非 base64 的 key 丟 `InvalidHostKey`,整個 Connect 就失敗)。問題由 `HostKeyAsker`(住在 UI 執行緒的 QObject,`host_key_asker()` 取得,兩個 SSH widget 建立時先建好)顯示:從連線執行緒問時走 `BlockingQueuedConnection`,連線執行緒等答案、UI 不等。每個問題都從「否」開始;發問的面板已經關掉(dock 關閉即刪除)就答「否」,不會沿用上一題的答案 | | `ssh_connect_thread.py` | `SshConnectThread(QThread)`:在自己的執行緒上跑一次會阻塞的 `connect()`,發 `connected` 或 `failed(message)`。`CONNECT_ERRORS` 是連線會丟的例外;`SHA1_ALGORITHMS` 是每個 `connect()` 都帶上的 `disabled_algorithms`,拒絕 SHA-1 的 RSA 簽章與金鑰交換(paramiko 5 已移除,paramiko 4 仍會提供;CVE-2026-44405)。TCP 10 秒、banner 15 秒、auth 30 秒逾時加起來,連不到的主機以前會把 IDE 凍住將近一分鐘 | | `ssh_key_loader.py` | 依序嘗試各種私鑰型別,回傳第一個能解析的;paramiko 不讀的 PKCS#8(`BEGIN PRIVATE KEY`/`BEGIN ENCRYPTED PRIVATE KEY`)由 `cryptography` 讀進來、在記憶體裡轉成 OpenSSH 格式再交給 paramiko;都不行時 `unloadable_key_reason()` 分辨是密語沒給/給錯(檔案有加密,而且給的密語解不開它;用 `cryptography` 試解)還是不支援的私鑰(例如加密的 DSA),或是要先匯出成 OpenSSH 格式的 PuTTY `.ppk` 金鑰 | @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2510 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2513 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index c5ed199b..4f7922f5 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -333,3 +333,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Tests**: the SFTP tree action and cancel tests, the menu-freeing and closed-panel tests pass (63). The full suite at `d96be79` passed (2490 passed, 15 skipped), and both startup tests exit 0. - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py` - **Open items**: none. + +## U-20260925-37 · 2026-09-25 · F2 and Delete in the SFTP tree · #feature #ssh #readme + +- **What**: the SFTP tree, like the project tree before U-20260925-35, renamed and deleted only through its right-click menu. +- **Change**: `SSHFileTreeManager` gives its tree F2 and Delete shortcuts (`WidgetShortcut`, so only while the tree has the focus) connected to the bound methods `_rename_current` and `_delete_current`, which run the menu's `action_rename` and `action_delete` on the entry in focus through `_act_on` (U-20260925-36): a dropped session is reported in a dialog, not raised. Delete asks first, No being the default. The menu's Rename and Delete entries show the keys (`_ENTRY_KEYS`, one map for both). +- **Tests**: new `TestTheKeys` in `test_sftp_tree_actions.py` (3): each key has one tree-scoped shortcut acting on the current entry, and a request failing on a dropped session is reported. All failed before the change. With the menu-freeing, closed-panel and cancel tests, 66 pass; the closed-panel test confirms the bound slots keep nothing alive. +- **Result / numbers**: 2513 tests in 141 files. `ruff check` clean; `C901` clean. +- **Docs**: the SSH Client paragraph of the three READMEs; `architecture_explore.md` (the SFTP tree, and the re-measured line counts it quotes: `ssh_file_viewer_widget.py` 756, `ssh/` 2,508, `diagram_editor_widget.py` 695). +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `test/test_utils/test_sftp_tree_actions.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 416f27d6..3fe3f495 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-37 | 2026-09-25 | F2 and Delete in the SFTP tree | #feature #ssh #readme | [2026-09-c](2026-09-c.md) | | U-20260925-36 | 2026-09-25 | Refactor: the SFTP tree's menu action runner and entry lookup | #refactor #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-35 | 2026-09-25 | F2 and Delete in the project tree | #feature #ui #readme | [2026-09-c](2026-09-c.md) | | U-20260925-34 | 2026-09-25 | The Regex tab's Traditional Chinese title | #fix #tools #i18n | [2026-09-c](2026-09-c.md) | @@ -439,4 +440,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 36 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 37 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index 5b8855a6..f081a5fe 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -12,6 +12,7 @@ from collections.abc import Callable from PySide6.QtCore import Qt, QThread, Signal +from PySide6.QtGui import QKeySequence, QShortcut from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QLineEdit, QTreeWidget, QTreeWidgetItem, QMenu, QFileDialog, QMessageBox, QSplitter, QInputDialog, QStyle @@ -92,6 +93,10 @@ def folder_item(item: QTreeWidgetItem | None) -> QTreeWidgetItem | None: return item.parent() +# The keys that rename and delete the entry in focus, by the menu entry they stand for +_ENTRY_KEYS = {"rename": Qt.Key.Key_F2, "delete": Qt.Key.Key_Delete} + + class SSHFileTreeManager(QWidget): """ QWidget: connection form + tree + context menu. @@ -140,6 +145,11 @@ def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: self.tree.itemExpanded.connect(self.on_item_expanded) self.tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.on_context_menu) + # F2 and Delete act on the entry in focus while the tree has the focus, as in the project tree + for name, act in (("rename", self._rename_current), ("delete", self._delete_current)): + shortcut = QShortcut(QKeySequence(_ENTRY_KEYS[name]), self.tree) + shortcut.setContext(Qt.ShortcutContext.WidgetShortcut) + shortcut.activated.connect(act) # Layouts splitter = QSplitter(Qt.Orientation.Vertical) @@ -405,7 +415,11 @@ def on_context_menu(self, pos): ("download", self.action_download), ("upload", self.action_upload), ): - handlers[menu.addAction(self.word_dict.get(f"ssh_file_viewer_context_menu_action_{name}"))] = handler + action = menu.addAction(self.word_dict.get(f"ssh_file_viewer_context_menu_action_{name}")) + if name in _ENTRY_KEYS: + action.setShortcut(QKeySequence(_ENTRY_KEYS[name])) # shown beside it: the tree's own keys do it + action.setShortcutVisibleInContextMenu(True) + handlers[action] = handler if self._transfer is not None and self._transfer.isRunning(): menu.addSeparator() cancel = menu.addAction(self.word_dict.get("ssh_file_viewer_context_menu_action_cancel_transfer")) @@ -417,6 +431,14 @@ def on_context_menu(self, pos): if handler is not None: self._act_on(handler, item) + def _rename_current(self) -> None: + """F2: rename the entry in focus.""" + self._act_on(self.action_rename, self._entry_of(self.tree.currentItem())) + + def _delete_current(self) -> None: + """Delete: delete the entry in focus, once the user says so (No is the default).""" + self._act_on(self.action_delete, self._entry_of(self.tree.currentItem())) + @staticmethod def _entry_of(item: QTreeWidgetItem | None) -> QTreeWidgetItem | None: """The entry *item* stands for: the "..." or loading row has none of its own, so its folder.""" diff --git a/test/test_utils/test_sftp_tree_actions.py b/test/test_utils/test_sftp_tree_actions.py index db53eef7..57cb9d28 100644 --- a/test/test_utils/test_sftp_tree_actions.py +++ b/test/test_utils/test_sftp_tree_actions.py @@ -371,3 +371,44 @@ def refuse(_path: str) -> None: _wait_for(lambda: _idle(widget)) assert len(shown) == 1 and "empty" not in shown[0] and "Permission denied" in shown[0] + + +class TestTheKeys: + """F2 renames and Delete deletes the entry in focus, as in the project tree (U-20260925-35).""" + + @staticmethod + def _shortcut(widget, keys: str): + from PySide6.QtCore import Qt + from PySide6.QtGui import QKeySequence, QShortcut + + found = [shortcut for shortcut in widget.tree.findChildren(QShortcut) if shortcut.key() == QKeySequence(keys)] + assert len(found) == 1, keys + assert found[0].context() == Qt.ShortcutContext.WidgetShortcut # only while the tree has the focus + return found[0] + + @pytest.mark.parametrize(("keys", "action"), [("F2", "action_rename"), ("Del", "action_delete")]) + def test_the_key_acts_on_the_current_entry(self, tree, monkeypatch, keys, action): + widget, _client, root, _warnings = tree + asked: list = [] + monkeypatch.setattr(widget, action, asked.append) + notes = _child(root, "notes.txt") + widget.tree.setCurrentItem(notes) + + self._shortcut(widget, keys).activated.emit() + + assert asked == [notes] + + def test_a_dropped_session_is_reported_not_raised(self, tree, monkeypatch): + widget, _client, root, _warnings = tree + reported: list = [] + monkeypatch.setattr(QMessageBox, "critical", lambda *args: reported.append(args[2])) + + def dropped(_item): + raise OSError("Socket is closed") + + monkeypatch.setattr(widget, "action_delete", dropped) + widget.tree.setCurrentItem(_child(root, "notes.txt")) + + self._shortcut(widget, "Del").activated.emit() + + assert reported and "Socket is closed" in reported[0] From 28c71f8bc87821505fdf9291d395c19e1a775ce9 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 03:56:24 +0800 Subject: [PATCH 089/166] Delete to the trash from the project tree --- README.md | 4 +- README/README_zh-CN.md | 4 +- README/README_zh-TW.md | 4 +- architecture_explore.md | 6 +- docs/updates/2026-09-c.md | 10 +++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 3 +- .../extend_traditional_chinese.py | 3 +- .../editor_main/file_tree_context_menu.py | 40 +++++++--- .../test_utils/test_file_tree_context_menu.py | 77 ++++++++++++++++++- 10 files changed, 131 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 82a9f0ac..e6c8207b 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Password or private-key authentication (the key file picked with Browse, startin ### And also -- **File Tree Context Menu** — right-click to create, rename, delete, copy absolute or relative paths, or reveal the item in your platform file manager (a file shown selected in Explorer and Finder). F2 renames and Delete deletes the item in focus while the tree has the focus; Delete asks first, with No as the default. Renaming or deleting a file open in an editor tab keeps the tab in sync. +- **File Tree Context Menu** — right-click to create, rename, delete, copy absolute or relative paths, or reveal the item in your platform file manager (a file shown selected in Explorer and Finder). F2 renames and Delete deletes the item in focus while the tree has the focus. A delete asks first, with No as the default, and moves the item to the trash (the Recycle Bin on Windows); where there is none, as on some network drives, it asks again before deleting for good. Renaming or deleting a file open in an editor tab keeps the tab in sync. - **Package Manager** — install automation modules and build tools from the menu, output in a run window. - **Integrated Documentation** — each module's docs and GitHub page open as in-IDE browser tabs (TestPioneer's documentation is its GitHub README). @@ -215,7 +215,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 755 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 756 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 442995d2..e70133e3 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -155,7 +155,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ### 其他 -- **文件树右键菜单** — 右键即可创建、重命名、删除、复制绝对或相对路径,或在系统的文件管理器中显示该项目(在 Explorer 与 Finder 中会选中该文件)。焦点在文件树时,F2 重命名、Delete 删除当前项目;Delete 会先询问,默认是“否”。重命名或删除一个已在编辑器标签页中打开的文件时,标签页会保持同步。 +- **文件树右键菜单** — 右键即可创建、重命名、删除、复制绝对或相对路径,或在系统的文件管理器中显示该项目(在 Explorer 与 Finder 中会选中该文件)。焦点在文件树时,F2 重命名、Delete 删除当前项目。删除前会先询问,默认是“否”,删除的项目会移到回收站(Windows 的回收站);没有回收站的地方(例如某些网络驱动器),会再询问一次才永久删除。重命名或删除一个已在编辑器标签页中打开的文件时,标签页会保持同步。 - **包管理器** — 从菜单安装自动化模块与构建工具,输出显示在运行窗口中。 - **集成文档** — 每个模块的文档与 GitHub 页面都以 IDE 内的浏览器标签页打开(TestPioneer 的文档就是它 GitHub 上的 README)。 @@ -215,7 +215,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 755 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 756 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 7bd3b12c..bdbd6eb8 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -155,7 +155,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ### 其他 -- **檔案樹右鍵選單** — 按右鍵即可建立、重新命名、刪除、複製絕對或相對路徑,或在系統的檔案管理員中顯示該項目(在 Explorer 與 Finder 中會選取該檔案)。焦點在檔案樹時,F2 重新命名、Delete 刪除目前的項目;Delete 會先詢問,預設是「否」。重新命名或刪除已在編輯器分頁中開啟的檔案時,分頁會同步更新。 +- **檔案樹右鍵選單** — 按右鍵即可建立、重新命名、刪除、複製絕對或相對路徑,或在系統的檔案管理員中顯示該項目(在 Explorer 與 Finder 中會選取該檔案)。焦點在檔案樹時,F2 重新命名、Delete 刪除目前的項目。刪除前會先詢問,預設是「否」,刪除的項目會移到回收筒(Windows 的資源回收筒);沒有回收筒的地方(例如某些網路磁碟),會再問一次才永久刪除。重新命名或刪除已在編輯器分頁中開啟的檔案時,分頁會同步更新。 - **套件管理員** — 從選單安裝自動化模組與建置工具,輸出顯示在執行視窗中。 - **整合文件** — 每個模組的文件與 GitHub 頁面都以 IDE 內的瀏覽器分頁開啟(TestPioneer 的文件就是它 GitHub 上的 README)。 @@ -215,7 +215,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 755 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 756 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index ee38bab1..6ae48a09 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -74,7 +74,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE - `add_menu_to_menubar()` — 建構全部選單(見 §5) - `syntax_extend_package()` — 註冊 `.json` / `.yml` / `.yaml` 自動化關鍵字高亮 - 依 `EDITOR_EXTEND_TAB` 註冊表加入外部擴充分頁(`_add_extend_tabs()`:每一個分頁各自建,建不起來的只記 log,不會讓整個 IDE 起不來) - - `setup_file_tree_context_menu()` — 掛上檔案樹右鍵選單,以及焦點在樹上時的 F2(重新命名)與 Delete(刪除,先問、預設否)快捷鍵(`_attach_keys()`,`WidgetShortcut`,走選單的同一組動作)。改名時開著的分頁跟著檔案走(改資料夾也一樣,底下每個開著的檔案都跟著走):先停掉分頁的自動存檔、改名、再用新路徑重開一條(`_stop_auto_save()` / `_start_auto_save()`)——JEditor 的存檔執行緒只認開檔當下的路徑,沒辦法改指向。外部修改監視也跟著搬(改名前就先移除,檔案搬走後 Windows 放不掉舊名),並照 `open_an_file` 重載語法高亮、git 基準與語言伺服器(`rename_self_tab()` 會清掉「未儲存」標記,有未存的編輯就用 `_on_text_changed()` 放回去,否則改名後五秒內關分頁會直接丟掉編輯);Dock Editor(`FullEditorWidget`,關閉時才寫回、檔案不存在就不寫)的 `current_file` 也改指新路徑(`_dock_editors_under()`)。新增與改名的名稱不能帶磁碟代號、根目錄、`..` 或 `:`,也不能解析到資料夾外(`_inside()`;改名只能是單一名稱)。刪除資料夾用 `remove_folder()`(唯讀檔清掉唯讀屬性再刪,git 的物件檔就是唯讀),符號連結與 junction 只刪連結本身。刪除時同樣用 `_editors_under()`:檔案或資料夾底下每個開著的分頁先停掉自動存檔,再刪;刪完只關掉檔案真的不見了的分頁,刪不掉(被鎖住、唯讀)的檔案分頁留著、自動存檔重開。「在檔案總管中顯示」由 `reveal_command()` 組指令:Windows 用 Explorer 的 `/select,`、macOS 用 `open -R` 把檔案選起來,其他平台 `xdg-open` 只能開資料夾;啟動失敗(例如沒有 `xdg-open`)經 `_perform_file_op()` 跳警告 + - `setup_file_tree_context_menu()` — 掛上檔案樹右鍵選單,以及焦點在樹上時的 F2(重新命名)與 Delete(刪除,先問、預設否)快捷鍵(`_attach_keys()`,`WidgetShortcut`,走選單的同一組動作)。改名時開著的分頁跟著檔案走(改資料夾也一樣,底下每個開著的檔案都跟著走):先停掉分頁的自動存檔、改名、再用新路徑重開一條(`_stop_auto_save()` / `_start_auto_save()`)——JEditor 的存檔執行緒只認開檔當下的路徑,沒辦法改指向。外部修改監視也跟著搬(改名前就先移除,檔案搬走後 Windows 放不掉舊名),並照 `open_an_file` 重載語法高亮、git 基準與語言伺服器(`rename_self_tab()` 會清掉「未儲存」標記,有未存的編輯就用 `_on_text_changed()` 放回去,否則改名後五秒內關分頁會直接丟掉編輯);Dock Editor(`FullEditorWidget`,關閉時才寫回、檔案不存在就不寫)的 `current_file` 也改指新路徑(`_dock_editors_under()`)。新增與改名的名稱不能帶磁碟代號、根目錄、`..` 或 `:`,也不能解析到資料夾外(`_inside()`;改名只能是單一名稱)。刪除先移到系統的回收筒(`_move_to_trash()`:`QFile.moveToTrash`,Windows 的資源回收筒、macOS 與 freedesktop 的垃圾桶);沒有回收筒可用時再問一次(預設否)才永久刪除,資料夾用 `remove_folder()`(唯讀檔清掉唯讀屬性再刪,git 的物件檔就是唯讀);符號連結與 junction 只刪連結本身,不進回收筒。刪除時同樣用 `_editors_under()`:檔案或資料夾底下每個開著的分頁先停掉自動存檔,再刪;刪完只關掉檔案真的不見了的分頁,刪不掉(被鎖住、唯讀)的檔案分頁留著、自動存檔重開。「在檔案總管中顯示」由 `reveal_command()` 組指令:Windows 用 Explorer 的 `/select,`、macOS 用 `open -R` 把檔案選起來,其他平台 `xdg-open` 只能開資料夾;啟動失敗(例如沒有 `xdg-open`)經 `_perform_file_op()` 跳警告 - `close_tab()` 覆寫 JEditor 的:分頁有 `may_close()` 就先問(提示詞編輯器、架構圖編輯器有未存的變更時會問);關掉的工具分頁 `deleteLater()`(JEditor 的 `removeTab` 不刪 widget,關過的工具分頁會留到 IDE 結束),JEditor 自己的編輯器分頁不動,關閉 IDE 時也先問過每個分頁與 dock,有一個說不就取消關閉 - `debug_mode=True` 時啟動 10 秒自動關閉 `QTimer`(CI 用) 3. `apply_stylesheet()` 套 qt_material 主題(預設 `dark_amber.xml`) @@ -369,7 +369,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 755 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 756 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2513 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2518 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 4f7922f5..1f8a032d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -343,3 +343,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the SSH Client paragraph of the three READMEs; `architecture_explore.md` (the SFTP tree, and the re-measured line counts it quotes: `ssh_file_viewer_widget.py` 756, `ssh/` 2,508, `diagram_editor_widget.py` 695). - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `test/test_utils/test_sftp_tree_actions.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-38 · 2026-09-25 · The project tree deletes to the trash · #feature #ui #readme + +- **What**: Delete in the project tree, from its menu or with the Delete key (U-20260925-35), removed the file or folder for good: a whole project folder was one Enter away from gone, with no Recycle Bin to take it back from. File managers, and editors such as VS Code, move to the trash by default. +- **Change**: `_action_delete` (`editor_main/file_tree_context_menu.py`), once confirmed, calls `_remove`: it moves the entry to the system's trash with `QFile.moveToTrash` (`_move_to_trash`; the Recycle Bin on Windows, the macOS and freedesktop trash elsewhere), and only where there is none (some network drives, a desktop without one) asks again, No being the default, before deleting for good (`remove_folder()` or `unlink()`). A symbolic link or junction is still removed itself, never moved, so what it points to stays. The confirmation reads "Move '{name}' to the trash?" (要把 '{name}' 移到回收筒嗎?) and the second question "'{name}' cannot be moved to the trash here. Delete it for good?". Measured here: `QFile.moveToTrash` moves a file and a folder to `C:\$Recycle.Bin`. +- **Tests**: new `TestDeletingToTheTrash` in `test_file_tree_context_menu.py` (5): a file and a folder go to the trash; with no trash the second question comes, defaulting to No, and Yes deletes for good while No keeps the file; the real `_move_to_trash` asks `QFile.moveToTrash`. An autouse fixture gives every test a trash of its own, so the suite does not fill the machine's Recycle Bin; the test of a failing delete runs where there is no trash, the only place a delete can fail now. With the rename, language, message box and default-button tests, 100 pass; `test_the_readmes_count_the_keys_there_are` caught the new key. +- **Result / numbers**: 2518 tests in 141 files; 756 keys in each dictionary. `ruff check` clean. +- **Docs**: the File Tree Context Menu line of the three READMEs and their key count; `architecture_explore.md`. +- **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_file_tree_context_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 3fe3f495..38116cd4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-38 | 2026-09-25 | The project tree deletes to the trash | #feature #ui #readme | [2026-09-c](2026-09-c.md) | | U-20260925-37 | 2026-09-25 | F2 and Delete in the SFTP tree | #feature #ssh #readme | [2026-09-c](2026-09-c.md) | | U-20260925-36 | 2026-09-25 | Refactor: the SFTP tree's menu action runner and entry lookup | #refactor #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-35 | 2026-09-25 | F2 and Delete in the project tree | #feature #ui #readme | [2026-09-c](2026-09-c.md) | @@ -440,4 +441,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 37 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 38 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 00b47cf0..96dc87c2 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -419,7 +419,8 @@ "file_tree_ctx_already_exists": "'{name}' already exists.", "file_tree_ctx_bad_name": "'{name}' is not a name inside this folder: it may not have a drive, start with a slash, or contain '..' or ':'.", "file_tree_ctx_confirm_delete": "Confirm Delete", - "file_tree_ctx_confirm_delete_message": "Are you sure you want to delete '{name}'?", + "file_tree_ctx_confirm_delete_message": "Move '{name}' to the trash?", + "file_tree_ctx_no_trash": "'{name}' cannot be moved to the trash here. Delete it for good?", # Diagram Editor — Menu "extend_tools_menu_diagram_editor_tab_action": "Diagram Editor Tab", "extend_tools_menu_diagram_editor_tab_label": "Diagram Editor", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index a56b979e..67b85b38 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -757,7 +757,8 @@ "file_tree_ctx_already_exists": "'{name}' 已存在。", "file_tree_ctx_bad_name": "「{name}」不是這個資料夾裡的名稱:不能有磁碟代號、不能以斜線開頭,也不能含有「..」或「:」。", "file_tree_ctx_confirm_delete": "確認刪除", - "file_tree_ctx_confirm_delete_message": "確定要刪除 '{name}' 嗎?", + "file_tree_ctx_confirm_delete_message": "要把 '{name}' 移到回收筒嗎?", + "file_tree_ctx_no_trash": "'{name}' 在這裡無法移到回收筒。要永久刪除嗎?", # Plugin Menu "plugin_menu_label": "外掛", "plugin_menu_about": "關於", diff --git a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py index aea7a7dc..1e3e5f0a 100644 --- a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py +++ b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py @@ -8,7 +8,7 @@ from collections.abc import Callable from pathlib import Path, PureWindowsPath -from PySide6.QtCore import Qt, QModelIndex +from PySide6.QtCore import QFile, Qt, QModelIndex from PySide6.QtGui import QKeySequence, QShortcut from PySide6.QtWidgets import ( QTreeView, QMenu, QFileSystemModel, QInputDialog, @@ -399,15 +399,7 @@ def _action_delete(tree_view: QTreeView, main_window, path: Path | None) -> None for editor, _file in open_tabs: _stop_auto_save(editor) - def _delete() -> None: - if _is_link(path): - _remove_link(path) - elif path.is_dir(): - remove_folder(path) - else: - path.unlink() - - _perform_file_op(tree_view, _delete) + _remove(tree_view, path) # Only a tab whose file is gone closes. The delete can fail -- a locked or # read-only file -- or remove only part of a folder, and closing the tabs # beforehand lost a file's tab and its unsaved edits while the file stayed. @@ -421,6 +413,34 @@ def _delete() -> None: main_window.tab_widget.removeTab(index) +def _move_to_trash(path: Path) -> bool: + """Move *path* to the system's trash (the Recycle Bin on Windows); ``False`` where there is none.""" + return QFile.moveToTrash(str(path)) + + +def _remove(tree_view: QTreeView, path: Path) -> None: + """Move *path* to the trash; where there is none, delete it for good if the user says so. + + A link (a symbolic link, a junction) is removed itself, never moved: what + it points to stays where it is. + """ + if _is_link(path): + _perform_file_op(tree_view, lambda: _remove_link(path)) + return + if _move_to_trash(path): + return + word = language_wrapper.language_word_dict + reply = QMessageBox.question( + tree_view, + word.get("file_tree_ctx_confirm_delete"), + as_text(word.get("file_tree_ctx_no_trash").format(name=str(path))), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if reply == QMessageBox.StandardButton.Yes: + _perform_file_op(tree_view, lambda: remove_folder(path) if path.is_dir() else path.unlink()) + + def _action_copy_path(tree_view: QTreeView, path: Path | None, relative: bool = False) -> None: if path is None: return diff --git a/test/test_utils/test_file_tree_context_menu.py b/test/test_utils/test_file_tree_context_menu.py index 80293240..67f36295 100644 --- a/test/test_utils/test_file_tree_context_menu.py +++ b/test/test_utils/test_file_tree_context_menu.py @@ -61,6 +61,27 @@ def confirm(monkeypatch, yes: bool) -> None: ctx.QMessageBox, "question", staticmethod(lambda *a, **k: button)) +# Before the stand-in below replaces it in every test +_REAL_MOVE_TO_TRASH = ctx._move_to_trash + + +@pytest.fixture(autouse=True) +def trash(tmp_path_factory, monkeypatch): + """A trash of the test's own: a delete must not fill the machine's Recycle Bin.""" + import shutil + + bin_folder = tmp_path_factory.mktemp("trash") + trashed: list[Path] = [] + + def move_to_trash(path: Path) -> bool: + trashed.append(path) + shutil.move(str(path), str(bin_folder / f"{len(trashed)}_{path.name}")) + return True + + monkeypatch.setattr(ctx, "_move_to_trash", move_to_trash) + return trashed + + @pytest.fixture() def warnings(monkeypatch): """Collect the warning dialogs an action raises instead of showing them.""" @@ -348,7 +369,9 @@ def under(_window, path): def test_a_delete_that_fails_keeps_the_tab_open(self, tree, tmp_path, monkeypatch): # The tabs used to close before the delete ran, so a locked file stayed - # on disk while its tab, and any unsaved edits in it, were gone. + # on disk while its tab, and any unsaved edits in it, were gone. Where + # there is no trash, the file is deleted for good, and that can fail. + monkeypatch.setattr(ctx, "_move_to_trash", lambda _path: False) target = tmp_path / "locked.py" target.touch() window = FakeWindow() @@ -375,6 +398,58 @@ def refuse(self, *args, **kwargs): assert restarted == [str(target)] +class TestDeletingToTheTrash: + """A delete from the tree was for good: it goes to the trash, as a file manager does.""" + + def test_a_file_goes_to_the_trash(self, tree, tmp_path, monkeypatch, trash): + target = tmp_path / "notes.py" + target.write_text("keep a copy", encoding="utf-8") + confirm(monkeypatch, yes=True) + + _action_delete(tree, FakeWindow(), target) + + assert trash == [target] + assert not target.exists() + + def test_a_folder_goes_to_the_trash_whole(self, tree, tmp_path, monkeypatch, trash): + folder = tmp_path / "pkg" + folder.mkdir() + (folder / "inner.py").touch() + confirm(monkeypatch, yes=True) + + _action_delete(tree, FakeWindow(), folder) + + assert trash == [folder] + + @pytest.mark.parametrize("delete_for_good", [True, False]) + def test_without_a_trash_it_asks_before_deleting_for_good(self, tree, tmp_path, monkeypatch, delete_for_good): + monkeypatch.setattr(ctx, "_move_to_trash", lambda _path: False) + target = tmp_path / "notes.py" + target.touch() + asked: list = [] + + def question(_parent, _title, text, _buttons, default): + asked.append((text, default)) + answer_now = delete_for_good or len(asked) == 1 # yes to the first question, then the choice + return QMessageBox.StandardButton.Yes if answer_now else QMessageBox.StandardButton.No + + monkeypatch.setattr(ctx.QMessageBox, "question", staticmethod(question)) + + _action_delete(tree, FakeWindow(), target) + + assert len(asked) == 2 + assert asked[1][1] == QMessageBox.StandardButton.No # for good is not the default + assert target.exists() is not delete_for_good + + def test_the_trash_is_the_systems(self, tmp_path, monkeypatch): + # The stand-in replaces _move_to_trash; the real one asks Qt for the system's trash + called: list = [] + monkeypatch.setattr(ctx.QFile, "moveToTrash", staticmethod(lambda name: called.append(name) or True)) + + assert _REAL_MOVE_TO_TRASH(tmp_path / "x.py") is True + assert called == [str(tmp_path / "x.py")] + + class TestCopyingThePath: def test_the_absolute_path_reaches_the_clipboard(self, tree, tmp_path): target = tmp_path / "module.py" From 5b6331fc1286998d3217949e2cacbcbae0f8ada6 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:00:24 +0800 Subject: [PATCH 090/166] Test every branch of the SSRF check --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- .../test_url_parser_differential.py | 32 +++++++++++ test/test_utils/test_url_validation.py | 54 +++++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 6ae48a09..d1d0b586 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2518 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2528 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 1f8a032d..f0e25e97 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -353,3 +353,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the File Tree Context Menu line of the three READMEs and their key count; `architecture_explore.md`. - **Files**: `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_file_tree_context_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-39 · 2026-09-25 · Tests that reach every branch of the SSRF check · #test #security #network + +- **What**: coverage of `utils/network/url_validation.py` showed branches of the SSRF check the suite never ran, among them its last line of defence: the comparison of the host urllib3 reads with the one `urlparse` reads (every differential URL in `test_url_parser_differential.py` stops earlier, at the character check), the `urlparse`-only parse failure, the fallback for a name IDNA cannot encode, a name that resolves to nothing, the de-duplication of repeated addresses, and the Teredo and NAT64 extraction in `_embedded_ipv4`. The last two never run on the Python versions CI covers, because the standard library already classes Teredo (`2001::/32`) as private and NAT64 (`64:ff9b::/96`) as reserved; they are there should a version class them otherwise, as 3.12 and 3.13 reclassified other ranges. +- **Change**: tests only. `test_url_parser_differential.py`: `http://[fe80::1%25eth0]/` passes the character check and is refused by the comparison (urllib3 decodes the zone ID, `urlparse` does not), with the ambiguous-host message; a `urlparse` failure becomes the unparsable-URL message without quoting the URL's token; `_as_ascii` leaves a name IDNA cannot encode as it is. `test_url_validation.py`: a name with no address is refused; repeated addresses come once, in the resolver's order; `_embedded_ipv4` finds 169.254.169.254 inside its IPv4-mapped, 6to4, Teredo and NAT64 wrappers and nothing inside a plain IPv6 address. Found while searching for URLs the two parsers read differently: none without a backslash, whitespace or a control character but the zone ID one; four others fail urllib3's parser first. +- **Result / numbers**: `url_validation.py` at 100% of lines and branches with these and `test_public_http.py` (69 pass). 2528 tests in 141 files. `ruff check` clean. +- **Files**: `test/test_utils/test_url_parser_differential.py`, `test/test_utils/test_url_validation.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 38116cd4..ffd2dd37 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-39 | 2026-09-25 | Tests that reach every branch of the SSRF check | #test #security #network | [2026-09-c](2026-09-c.md) | | U-20260925-38 | 2026-09-25 | The project tree deletes to the trash | #feature #ui #readme | [2026-09-c](2026-09-c.md) | | U-20260925-37 | 2026-09-25 | F2 and Delete in the SFTP tree | #feature #ssh #readme | [2026-09-c](2026-09-c.md) | | U-20260925-36 | 2026-09-25 | Refactor: the SFTP tree's menu action runner and entry lookup | #refactor #ssh | [2026-09-c](2026-09-c.md) | @@ -441,4 +442,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 38 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 39 | diff --git a/test/test_utils/test_url_parser_differential.py b/test/test_utils/test_url_parser_differential.py index 80e0307a..a757a7a3 100644 --- a/test/test_utils/test_url_parser_differential.py +++ b/test/test_utils/test_url_parser_differential.py @@ -86,3 +86,35 @@ def serve() -> None: server.join(5) assert served == [] + + +def test_the_two_readings_are_compared_past_the_character_check(public_dns): + # Every URL above stops at the character check; this one passes it, and only + # the comparison sees that urllib3 decodes the zone ID and urlparse does not + from pybreeze.utils.exception.exception_tags import url_ambiguous_host_error + + with pytest.raises(UnsafeURLError) as raised: + validate_url("http://[fe80::1%25eth0]/") + + assert str(raised.value) == url_ambiguous_host_error + + +def test_a_url_only_urlparse_refuses_is_refused_without_quoting_it(public_dns, monkeypatch): + # No URL urllib3 takes is known to fail urlparse, but one would have escaped + # into the Qt slot as a ValueError quoting the whole URL + from pybreeze.utils.exception.exception_tags import url_unparsable_error + + def refuse(url): + raise ValueError(f"netloc {url!r} contains invalid characters") + + monkeypatch.setattr(url_validation, "urlparse", refuse) + + with pytest.raises(UnsafeURLError) as raised: + validate_url("https://example.com/?token=sk-live-not-a-real-key") + + assert str(raised.value) == url_unparsable_error + + +def test_a_name_idna_cannot_encode_is_left_as_it_is(): + # It then fails the comparison or the lookup; it is not changed into another name + assert url_validation._as_ascii("a☕b.com") == "a☕b.com" diff --git a/test/test_utils/test_url_validation.py b/test/test_utils/test_url_validation.py index c2b170d4..50c22113 100644 --- a/test/test_utils/test_url_validation.py +++ b/test/test_utils/test_url_validation.py @@ -79,3 +79,57 @@ class TestHostnamesThatCannotBeLookedUp: def test_an_impossible_hostname_is_refused_like_any_other(self, url): with pytest.raises(UnsafeURLError): validate_url(url) + + +class TestTheAddressesOfAName: + @staticmethod + def _resolving(monkeypatch, *addresses): + import socket + + from pybreeze.utils.network import url_validation + + answer = [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (address, 0)) for address in addresses] + monkeypatch.setattr(url_validation.socket, "getaddrinfo", lambda *_a, **_k: answer) + + def test_a_name_with_no_address_is_refused(self, monkeypatch): + from pybreeze.utils.network.url_validation import public_addresses + + self._resolving(monkeypatch) + with pytest.raises(UnsafeURLError, match="Cannot resolve hostname 'example.com'"): + public_addresses("example.com") + + def test_each_address_comes_once_in_the_resolvers_order(self, monkeypatch): + # The resolver repeats an address once per socket type it was not asked to filter + from pybreeze.utils.network.url_validation import public_addresses + + self._resolving(monkeypatch, "93.184.215.14", "93.184.215.14", "8.8.8.8", "93.184.215.14") + assert public_addresses("example.com") == ["93.184.215.14", "8.8.8.8"] + + +class TestTheIPv4InsideAnIPv6Wrapper: + """The standard library already refuses Teredo (private) and NAT64 (reserved) addresses. + + ``_embedded_ipv4`` is the check behind that, should a Python version class + them otherwise, as 3.12 and 3.13 reclassified other ranges: it must find + the IPv4 each wrapper routes to, and the suite never reached it. + """ + + @pytest.mark.parametrize(("wrapped", "inside"), [ + ("::ffff:169.254.169.254", "169.254.169.254"), # IPv4-mapped + ("2002:a9fe:a9fe::1", "169.254.169.254"), # 6to4 + ("2001:0:0:0:0:0:5601:5601", "169.254.169.254"), # Teredo: the client, inverted + ("64:ff9b::a9fe:a9fe", "169.254.169.254"), # NAT64 well-known prefix + ]) + def test_the_address_it_routes_to_is_found(self, wrapped, inside): + import ipaddress + + from pybreeze.utils.network.url_validation import _embedded_ipv4 + + assert ipaddress.ip_address(inside) in _embedded_ipv4(ipaddress.ip_address(wrapped)) + + def test_a_plain_ipv6_address_wraps_nothing(self): + import ipaddress + + from pybreeze.utils.network.url_validation import _embedded_ipv4 + + assert _embedded_ipv4(ipaddress.ip_address("2606:4700:4700::1111")) == [] From 5de78210d52f4752647a8d9158f3606b182f9770 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:06:13 +0800 Subject: [PATCH 091/166] Test every branch of the pinned connections and capped reads --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- test/test_utils/test_http_client.py | 51 ++++++++++++++++++ test/test_utils/test_public_http.py | 83 +++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index d1d0b586..cc69a1bb 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2528 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2539 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index f0e25e97..a6580e21 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -361,3 +361,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: `url_validation.py` at 100% of lines and branches with these and `test_public_http.py` (69 pass). 2528 tests in 141 files. `ruff check` clean. - **Files**: `test/test_utils/test_url_parser_differential.py`, `test/test_utils/test_url_validation.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-40 · 2026-09-25 · Tests that reach every branch of the pinned connections and capped reads · #test #security #network + +- **What**: after U-20260925-39, the other two modules every outbound request goes through still had behaviour no test ran. `public_http.py`: when every checked address fails, the last failure is raised (the requests and the urllib path); HTTPS through a proxy is left to the proxy (urllib); and the overall deadline's promises: a block that returns after the time is up still raises `ReadTimeout`, a socket watched after the time is up is shut at once, a timer firing after the request ended shuts nothing, and there is nothing to watch without a socket. `http_client.py`: empty chunks are skipped, a read error the watchdog did not cause is raised (the response still closed), a cancelled watchdog does nothing when its time comes, and a connection it cannot shut down is logged, not raised on the timer's thread. +- **Change**: tests only, in `test_public_http.py` (7: two in `TestEveryCheckedAddressIsTried`, one in `TestUrllibOpener`, four in the new `TestTheDeadlineItself`) and `test_http_client.py` (4). The proxy test sees the opener send `CONNECT rebind.test:443` to a local proxy. +- **Result / numbers**: `public_http.py`, `http_client.py` and `url_validation.py` all at 100% of lines and branches (159 pass across their tests). 2539 tests in 141 files. `ruff check` clean. +- **Files**: `test/test_utils/test_public_http.py`, `test/test_utils/test_http_client.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index ffd2dd37..64d92e62 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-40 | 2026-09-25 | Tests that reach every branch of the pinned connections and capped reads | #test #security #network | [2026-09-c](2026-09-c.md) | | U-20260925-39 | 2026-09-25 | Tests that reach every branch of the SSRF check | #test #security #network | [2026-09-c](2026-09-c.md) | | U-20260925-38 | 2026-09-25 | The project tree deletes to the trash | #feature #ui #readme | [2026-09-c](2026-09-c.md) | | U-20260925-37 | 2026-09-25 | F2 and Delete in the SFTP tree | #feature #ssh #readme | [2026-09-c](2026-09-c.md) | @@ -442,4 +443,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 39 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 40 | diff --git a/test/test_utils/test_http_client.py b/test/test_utils/test_http_client.py index d83a3722..c02e5cc1 100644 --- a/test/test_utils/test_http_client.py +++ b/test/test_utils/test_http_client.py @@ -188,3 +188,54 @@ def test_the_deadline_reads_as_a_timeout_to_the_user(self): from pybreeze.utils.network.http_client import describe_request_error assert "timed out" in describe_request_error(requests.exceptions.ReadTimeout("slow")) + + +class TestWhatTheReadSkipsAndPassesOn: + def test_empty_chunks_are_skipped(self): + # A chunked or kept-alive stream can hand back an empty chunk between real ones + class Gappy(FakeResponse): + def iter_content(self, chunk_size: int = 65536): + yield from (b"", b"ab", b"", b"c") + + assert read_capped_text(Gappy(b""), max_bytes=10) == "abc" + + def test_an_error_the_watchdog_did_not_cause_is_raised(self): + import requests + + class Broken(FakeResponse): + def iter_content(self, chunk_size: int = 65536): + yield b"ab" + raise requests.exceptions.ChunkedEncodingError("connection broken") + + response = Broken(b"") + with pytest.raises(requests.exceptions.ChunkedEncodingError): + read_capped_text(response, max_bytes=10) + assert response.closed + + +class TestTheWatchdog: + def test_a_cancelled_watchdog_does_nothing_when_its_time_comes(self): + from pybreeze.utils.network.http_client import _Watchdog + + response = FakeResponse(b"") + watchdog = _Watchdog(response, 60) + watchdog.cancel() + watchdog._fire() # the timer, had it fired late + + assert not watchdog.fired + assert not response.closed + + def test_a_connection_that_cannot_be_shut_is_logged_not_raised(self): + # On the timer's thread an exception would go nowhere and leave the read waiting + from pybreeze.utils.network.http_client import _Watchdog + + def refuse(): + raise OSError("already closed") + + response = FakeResponse(b"") + response.raw = SimpleNamespace(shutdown=refuse) + watchdog = _Watchdog(response, 60) + watchdog._fire() + + assert watchdog.fired + watchdog.cancel() diff --git a/test/test_utils/test_public_http.py b/test/test_utils/test_public_http.py index d5afb912..08e17d78 100644 --- a/test/test_utils/test_public_http.py +++ b/test/test_utils/test_public_http.py @@ -6,9 +6,11 @@ """ from __future__ import annotations +import http.client import socket import ssl import threading +import time import urllib.error import urllib.request @@ -201,6 +203,18 @@ def test_a_proxy_on_the_local_network_is_still_used(self, dns, listener): assert listener.received[0].startswith(b"GET http://rebind.test/i.png HTTP/1.1\r\n") + def test_https_through_a_proxy_is_left_to_the_proxy(self, dns, listener): + # The proxy connects to the name; there is no address here to pin + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({"https": f"http://127.0.0.1:{listener.port}"}), + PublicHTTPHandler(), PublicHTTPSHandler()) + + with pytest.raises((urllib.error.URLError, ssl.SSLError, ConnectionError, http.client.HTTPException)): + opener.open("https://rebind.test/i.png", timeout=3) + + listener.close() + assert listener.received[0].startswith(b"CONNECT rebind.test:443 HTTP/1.") + @pytest.fixture() def two_addresses(monkeypatch): @@ -228,6 +242,26 @@ def test_urllib_goes_on_to_the_next_address(self, two_addresses, loopback_allowe with opener.open(f"http://two.test:{listener.port}/", timeout=3) as response: assert response.read() == b"ok" + @staticmethod + def _closed_port() -> int: + """A loopback port nothing listens on any more.""" + probe = socket.socket() + probe.bind(("127.0.0.1", 0)) + port = probe.getsockname()[1] + probe.close() + return port + + def test_requests_fails_when_no_address_answers(self, two_addresses, loopback_allowed): + # After the last address the error is the connection's own, not a hang or a None raised + with _session() as session, pytest.raises(requests.ConnectionError): + session.get(f"http://two.test:{self._closed_port()}/", timeout=(3, 3)) + + def test_urllib_fails_when_no_address_answers(self, two_addresses, loopback_allowed): + opener = urllib.request.build_opener(PublicHTTPHandler()) + + with pytest.raises(urllib.error.URLError): + opener.open(f"http://two.test:{self._closed_port()}/", timeout=3) + def test_one_blocked_address_still_refuses_the_name(self, two_addresses, listener): with _session() as session, pytest.raises(requests.ConnectionError): session.get(f"http://two.test:{listener.port}/", timeout=(3, 3)) @@ -251,6 +285,55 @@ def getaddrinfo(host, port=None, *args, **kwargs): assert looked_up == ["xn--strae-oqa.de"] +class TestTheDeadlineItself: + """What ``overall_deadline`` promises in the cases no request above runs into.""" + + @staticmethod + def _passed(seconds: float = 0.0): + from pybreeze.utils.network.public_http import _Deadline + + deadline = _Deadline(seconds) + limit = time.monotonic() + 5 + while not deadline.passed and time.monotonic() < limit: + time.sleep(0.01) + return deadline + + def test_a_block_that_ends_after_the_time_is_up_still_times_out(self): + # A read cut off where no length was announced returns normally: the block must not + from pybreeze.utils.network.public_http import overall_deadline + + with pytest.raises(requests.exceptions.ReadTimeout), overall_deadline(0.01): + time.sleep(0.2) + + def test_a_socket_watched_after_the_time_is_up_is_shut_at_once(self): + deadline = self._passed() + mine, theirs = socket.socketpair() + with mine, theirs: + deadline.watch(mine) + theirs.settimeout(2) + assert theirs.recv(1) == b"" # the other end sees it closed + deadline.end() + + def test_nothing_is_shut_once_the_request_is_over(self): + from pybreeze.utils.network.public_http import _Deadline + + deadline = _Deadline(60) + mine, theirs = socket.socketpair() + with mine, theirs: + deadline.watch(mine) + deadline.end() + deadline._pass() # the timer firing late, after end() + assert not deadline.passed + mine.sendall(b"x") + theirs.settimeout(2) + assert theirs.recv(1) == b"x" + + def test_no_socket_is_nothing_to_watch(self): + deadline = self._passed() + deadline.watch(None) + deadline.end() + + class TestTheOverallDeadline: """A read timeout restarts with every byte; the deadline bounds the whole request.""" From 6cd731c3a7fab27940cc6a9217eddc7f9ab84b1f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:08:01 +0800 Subject: [PATCH 092/166] Record cryptography among the undeclared direct imports --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- progress.md | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index a6580e21..f80eec9d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -369,3 +369,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: `public_http.py`, `http_client.py` and `url_validation.py` all at 100% of lines and branches (159 pass across their tests). 2539 tests in 141 files. `ruff check` clean. - **Files**: `test/test_utils/test_public_http.py`, `test/test_utils/test_http_client.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-41 · 2026-09-25 · Record cryptography with the undeclared direct imports in progress #53 · #docs #deps #security + +- **What**: a check of 2026's cryptography advisories found that PyBreeze imports `cryptography` directly (`connect_gui/ssh/ssh_key_loader.py`: `serialization`, `UnsupportedAlgorithm`, `CryptographyDeprecationWarning`; used more since U-20260925-07 reads PKCS#8 keys with it), yet declares it nowhere: it arrives through paramiko, as `requests` and `urllib3` arrive through the automation packages (progress #53). This repo's `.venv` has 46.0.6; 46.0.7 fixes CVE-2026-39892, a buffer over-read when a non-contiguous buffer (such as `buf[::-1]`) is passed to an API taking a buffer, like `Hash.update()`. The key loader passes whole `bytes`, so its calls are not affected; CVE-2026-34073 (X.509 name constraints) and CVE-2026-69247 to 69249 (PKCS#7, name constraints, path building) concern APIs PyBreeze does not call. +- **Change**: `progress.md` #53 names `cryptography` beside `requests` and `urllib3`, and the question now includes `cryptography>=46.0.7`. Declaring or pinning it stays the owner's decision, as for the other two. +- **Files**: `progress.md` +- **Evidence**: [oss-sec: cryptography 46.0.7, CVE-2026-39892](https://seclists.org/oss-sec/2026/q2/51); [CVE-2026-34073](https://www.sentinelone.com/vulnerability-database/cve-2026-34073/). +- **Open items**: `progress.md` #53. diff --git a/docs/updates/README.md b/docs/updates/README.md index 64d92e62..7e3874fd 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-41 | 2026-09-25 | Record cryptography with the undeclared direct imports in progress #53 | #docs #deps #security | [2026-09-c](2026-09-c.md) | | U-20260925-40 | 2026-09-25 | Tests that reach every branch of the pinned connections and capped reads | #test #security #network | [2026-09-c](2026-09-c.md) | | U-20260925-39 | 2026-09-25 | Tests that reach every branch of the SSRF check | #test #security #network | [2026-09-c](2026-09-c.md) | | U-20260925-38 | 2026-09-25 | The project tree deletes to the trash | #feature #ui #readme | [2026-09-c](2026-09-c.md) | @@ -443,4 +444,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 40 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 41 | diff --git a/progress.md b/progress.md index 9b601813..5e22dea9 100644 --- a/progress.md +++ b/progress.md @@ -9,7 +9,7 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#2** [DECIDE] Closing one run window leaves its child running, with no window and no way to stop it short of a task manager; only closing the whole IDE stops it (`PyBreezeMainWindow.closeEvent`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`). Should closing a run window stop its run (`CodeWindow.stop_runner()` exists; `CodeWindow.closeEvent` today only lets the main window forget a finished one), ask first, or keep going on purpose (for example a long load test that mails its report)? - **#27** [DECIDE] paramiko is not pinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), so an install may get 4.x, which still has CVE-2026-44405 (SHA-1 RSA signatures). The code refuses SHA-1 on every connect whatever the version (`SHA1_ALGORITHMS`, U-20260923-40), and the SSH tests pass on 5.0.0. Should the dependency say `paramiko>=5.0.0`, or be pinned exactly like PySide6? - **#49** [DECIDE] jupyterlab is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`) and the JupyterLab tab installs it only when it is missing (`jupyter_lab_gui/jupyter_lab_thread.py`, `is_jupyter_installed`), so an existing environment keeps whatever jupyter_server it has; this repo's `.venv` has 2.17.0. Fixed upstream: path traversal (CVE-2026-5422, fixed in 2.18.2), stored XSS through the nbconvert handlers (CVE-2026-44727 / GHSA-fcw5-x6j4-ccmp, 2.20.0; the embedded server has no token, so a script in a rendered notebook can drive it) and tokens logged from the Referer on 5xx (CVE-2026-86049, 2.21.0). jupyterlab itself is 4.5.6 there, below the fixes for a sanitizer-allowed command-linker button that runs commands on one click (CVE-2026-42557, 4.5.7), an SVG opened in the image viewer keeping a same-origin script context (CVE-2026-73415) and an imported `overrides.json` (CVE-2026-73417), both fixed in 4.5.10 / 4.6.2; with no token, a script running in the page can drive the server. Should the dependencies require `jupyter_server>=2.21.0` and `jupyterlab>=4.5.10`, and should the tab check the installed version and offer to upgrade? -- **#53** [DECIDE] `requests` and `urllib3` are imported directly (`utils/network/public_http.py`, `http_client.py`, `url_validation.py`, the three AI panels) but declared nowhere (`requirements.txt`, `pyproject.toml`, `dev.toml`); they arrive through the automation packages, so their versions are whatever those allow. This repo's `.venv` has urllib3 2.6.3, below the 2.7.0 that fixes CVE-2026-44431 (Authorization and Cookie forwarded on a cross-origin redirect; these requests do not follow redirects) and CVE-2026-44432 (a Brotli response decompressed whole on a second `read(amt)`; not reproduced here with brotli 1.2.0 installed, `read_capped_text` stopped at its 16 MB cap). `public_http` also relies on urllib3's `_dns_host` and `http.client`'s `_create_connection` (`test_public_http.py` guards both; passes on urllib3 2.6.3 and 2.8.0). Should the dependencies declare `requests` and `urllib3>=2.7.0`, and pin them like PySide6? +- **#53** [DECIDE] `requests` and `urllib3` are imported directly (`utils/network/public_http.py`, `http_client.py`, `url_validation.py`, the three AI panels) but declared nowhere (`requirements.txt`, `pyproject.toml`, `dev.toml`); they arrive through the automation packages, so their versions are whatever those allow. This repo's `.venv` has urllib3 2.6.3, below the 2.7.0 that fixes CVE-2026-44431 (Authorization and Cookie forwarded on a cross-origin redirect; these requests do not follow redirects) and CVE-2026-44432 (a Brotli response decompressed whole on a second `read(amt)`; not reproduced here with brotli 1.2.0 installed, `read_capped_text` stopped at its 16 MB cap). `public_http` also relies on urllib3's `_dns_host` and `http.client`'s `_create_connection` (`test_public_http.py` guards both; passes on urllib3 2.6.3 and 2.8.0). `cryptography` is imported directly too (`connect_gui/ssh/ssh_key_loader.py`, reading PKCS#8 and passphrase-protected keys) and arrives only through paramiko; the `.venv` has 46.0.6, below 46.0.7, which fixes CVE-2026-39892 (a non-contiguous buffer passed to an API that takes one, such as `Hash.update(buf[::-1])`; the key loader passes whole `bytes`). Should the dependencies declare `requests`, `urllib3>=2.7.0` and `cryptography>=46.0.7`, and pin them like PySide6? - **#54** [DECIDE] The release path trusts more than it needs to. `stable.yml`'s `publish` job uploads with a long-lived `secrets.PYPI_API_TOKEN` (`.github/workflows/stable.yml:136-140`); PyPI's trusted publishing (OIDC, `pypa/gh-action-pypi-publish` with `id-token: write`) would need no stored token, but it has to be set up on PyPI by the owner first. Every action in `dev.yml` and `stable.yml` is pinned by a movable tag (`actions/checkout@v4`, `SonarSource/sonarqube-scan-action@v8.2.1`, ...), not a commit SHA, and the job holding the PyPI token and `contents: write` runs them. CI also installs prthinker from the head of its `main` branch (`dev.yml:39`, `stable.yml:39`). Should the publish job move to trusted publishing and the actions be pinned by SHA? - **#89** [DECIDE] The prthinker settings offer the Gemini, Cohere and Mistral backends but have no field for their keys: prthinker reads `PRTHINKER_GEMINI_API_KEY` / `_COHERE_` / `_MISTRAL_` from the environment the IDE was started in (as `test_prthinker_contract.py` notes), so choosing one of them in the dialog alone always fails for want of a key (`dialog/prthinker_setting_dialog.py`, `extend/prthinker_extend/prthinker_setting.py`). Should they get password fields like the OpenAI and Anthropic keys, or the dialog say where the key comes from? Related: a saved backend, platform or RAG value the dialog does not list is shown as the first item and replaced on the next Save, on purpose (`test_a_stored_value_that_is_not_on_offer_leaves_the_first_choice`); a newer prthinker's value is lost that way. Keep it, or list the unknown value? - **#92** [DECIDE] The embedded JupyterLab has no token (`--ServerApp.token=`): the loopback bind keeps browsers and other machines out, but not other accounts on a shared machine (RDS, a lab Linux box), which get a kernel as the IDE user. Generate a token per launch and load the view with it? The Security › JupyterLab rule would change with it. From 0e043adb2d01482439b8b62f791a8c270b9fce13 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:08:36 +0800 Subject: [PATCH 093/166] Record qt_material among the undeclared direct imports --- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- progress.md | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index f80eec9d..087c3fdd 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -377,3 +377,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `progress.md` - **Evidence**: [oss-sec: cryptography 46.0.7, CVE-2026-39892](https://seclists.org/oss-sec/2026/q2/51); [CVE-2026-34073](https://www.sentinelone.com/vulnerability-database/cve-2026-34073/). - **Open items**: `progress.md` #53. + +## U-20260925-42 · 2026-09-25 · Every undeclared direct import in progress #53 · #docs #deps + +- **What**: after U-20260925-41, every top-level import in `pybreeze/` was compared with `requirements.txt`. Besides `requests`, `urllib3` and `cryptography`, one more third-party package is imported but declared nowhere: `qt_material`, whose `apply_stylesheet` `start_editor` calls to style the window (`editor_main/main_ui.py`); it arrives only through je-editor, so a je-editor that dropped it would stop the IDE starting. `idna` (`utils/network/url_validation.py`) is imported with a fallback, and `shiboken6` is installed with PySide6. +- **Change**: `progress.md` #53 names `qt_material` and asks about declaring `qt-material` too. +- **Files**: `progress.md` +- **Open items**: `progress.md` #53. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7e3874fd..b70b06e6 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-42 | 2026-09-25 | Every undeclared direct import in progress #53 | #docs #deps | [2026-09-c](2026-09-c.md) | | U-20260925-41 | 2026-09-25 | Record cryptography with the undeclared direct imports in progress #53 | #docs #deps #security | [2026-09-c](2026-09-c.md) | | U-20260925-40 | 2026-09-25 | Tests that reach every branch of the pinned connections and capped reads | #test #security #network | [2026-09-c](2026-09-c.md) | | U-20260925-39 | 2026-09-25 | Tests that reach every branch of the SSRF check | #test #security #network | [2026-09-c](2026-09-c.md) | @@ -444,4 +445,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 41 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 42 | diff --git a/progress.md b/progress.md index 5e22dea9..c79429d4 100644 --- a/progress.md +++ b/progress.md @@ -9,7 +9,7 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#2** [DECIDE] Closing one run window leaves its child running, with no window and no way to stop it short of a task manager; only closing the whole IDE stops it (`PyBreezeMainWindow.closeEvent`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`). Should closing a run window stop its run (`CodeWindow.stop_runner()` exists; `CodeWindow.closeEvent` today only lets the main window forget a finished one), ask first, or keep going on purpose (for example a long load test that mails its report)? - **#27** [DECIDE] paramiko is not pinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), so an install may get 4.x, which still has CVE-2026-44405 (SHA-1 RSA signatures). The code refuses SHA-1 on every connect whatever the version (`SHA1_ALGORITHMS`, U-20260923-40), and the SSH tests pass on 5.0.0. Should the dependency say `paramiko>=5.0.0`, or be pinned exactly like PySide6? - **#49** [DECIDE] jupyterlab is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`) and the JupyterLab tab installs it only when it is missing (`jupyter_lab_gui/jupyter_lab_thread.py`, `is_jupyter_installed`), so an existing environment keeps whatever jupyter_server it has; this repo's `.venv` has 2.17.0. Fixed upstream: path traversal (CVE-2026-5422, fixed in 2.18.2), stored XSS through the nbconvert handlers (CVE-2026-44727 / GHSA-fcw5-x6j4-ccmp, 2.20.0; the embedded server has no token, so a script in a rendered notebook can drive it) and tokens logged from the Referer on 5xx (CVE-2026-86049, 2.21.0). jupyterlab itself is 4.5.6 there, below the fixes for a sanitizer-allowed command-linker button that runs commands on one click (CVE-2026-42557, 4.5.7), an SVG opened in the image viewer keeping a same-origin script context (CVE-2026-73415) and an imported `overrides.json` (CVE-2026-73417), both fixed in 4.5.10 / 4.6.2; with no token, a script running in the page can drive the server. Should the dependencies require `jupyter_server>=2.21.0` and `jupyterlab>=4.5.10`, and should the tab check the installed version and offer to upgrade? -- **#53** [DECIDE] `requests` and `urllib3` are imported directly (`utils/network/public_http.py`, `http_client.py`, `url_validation.py`, the three AI panels) but declared nowhere (`requirements.txt`, `pyproject.toml`, `dev.toml`); they arrive through the automation packages, so their versions are whatever those allow. This repo's `.venv` has urllib3 2.6.3, below the 2.7.0 that fixes CVE-2026-44431 (Authorization and Cookie forwarded on a cross-origin redirect; these requests do not follow redirects) and CVE-2026-44432 (a Brotli response decompressed whole on a second `read(amt)`; not reproduced here with brotli 1.2.0 installed, `read_capped_text` stopped at its 16 MB cap). `public_http` also relies on urllib3's `_dns_host` and `http.client`'s `_create_connection` (`test_public_http.py` guards both; passes on urllib3 2.6.3 and 2.8.0). `cryptography` is imported directly too (`connect_gui/ssh/ssh_key_loader.py`, reading PKCS#8 and passphrase-protected keys) and arrives only through paramiko; the `.venv` has 46.0.6, below 46.0.7, which fixes CVE-2026-39892 (a non-contiguous buffer passed to an API that takes one, such as `Hash.update(buf[::-1])`; the key loader passes whole `bytes`). Should the dependencies declare `requests`, `urllib3>=2.7.0` and `cryptography>=46.0.7`, and pin them like PySide6? +- **#53** [DECIDE] `requests` and `urllib3` are imported directly (`utils/network/public_http.py`, `http_client.py`, `url_validation.py`, the three AI panels) but declared nowhere (`requirements.txt`, `pyproject.toml`, `dev.toml`); they arrive through the automation packages, so their versions are whatever those allow. This repo's `.venv` has urllib3 2.6.3, below the 2.7.0 that fixes CVE-2026-44431 (Authorization and Cookie forwarded on a cross-origin redirect; these requests do not follow redirects) and CVE-2026-44432 (a Brotli response decompressed whole on a second `read(amt)`; not reproduced here with brotli 1.2.0 installed, `read_capped_text` stopped at its 16 MB cap). `public_http` also relies on urllib3's `_dns_host` and `http.client`'s `_create_connection` (`test_public_http.py` guards both; passes on urllib3 2.6.3 and 2.8.0). `cryptography` is imported directly too (`connect_gui/ssh/ssh_key_loader.py`, reading PKCS#8 and passphrase-protected keys) and arrives only through paramiko; the `.venv` has 46.0.6, below 46.0.7, which fixes CVE-2026-39892 (a non-contiguous buffer passed to an API that takes one, such as `Hash.update(buf[::-1])`; the key loader passes whole `bytes`). So is `qt_material`, which `start_editor` needs to style the window (`editor_main/main_ui.py`) and which arrives only through je-editor; `idna` (`utils/network/url_validation.py`) is imported with a fallback. Should the dependencies declare `requests`, `urllib3>=2.7.0`, `cryptography>=46.0.7` and `qt-material`, and pin them like PySide6? - **#54** [DECIDE] The release path trusts more than it needs to. `stable.yml`'s `publish` job uploads with a long-lived `secrets.PYPI_API_TOKEN` (`.github/workflows/stable.yml:136-140`); PyPI's trusted publishing (OIDC, `pypa/gh-action-pypi-publish` with `id-token: write`) would need no stored token, but it has to be set up on PyPI by the owner first. Every action in `dev.yml` and `stable.yml` is pinned by a movable tag (`actions/checkout@v4`, `SonarSource/sonarqube-scan-action@v8.2.1`, ...), not a commit SHA, and the job holding the PyPI token and `contents: write` runs them. CI also installs prthinker from the head of its `main` branch (`dev.yml:39`, `stable.yml:39`). Should the publish job move to trusted publishing and the actions be pinned by SHA? - **#89** [DECIDE] The prthinker settings offer the Gemini, Cohere and Mistral backends but have no field for their keys: prthinker reads `PRTHINKER_GEMINI_API_KEY` / `_COHERE_` / `_MISTRAL_` from the environment the IDE was started in (as `test_prthinker_contract.py` notes), so choosing one of them in the dialog alone always fails for want of a key (`dialog/prthinker_setting_dialog.py`, `extend/prthinker_extend/prthinker_setting.py`). Should they get password fields like the OpenAI and Anthropic keys, or the dialog say where the key comes from? Related: a saved backend, platform or RAG value the dialog does not list is shown as the first item and replaced on the next Save, on purpose (`test_a_stored_value_that_is_not_on_offer_leaves_the_first_choice`); a newer prthinker's value is lost that way. Keep it, or list the unknown value? - **#92** [DECIDE] The embedded JupyterLab has no token (`--ServerApp.token=`): the loopback bind keeps browsers and other machines out, but not other accounts on a shared machine (RDS, a lab Linux box), which get a kernel as the IDE user. Generate a token per launch and load the view with it? The Security › JupyterLab rule would change with it. From 5c07739bfcbbc7b7f7e4880c0b62c62e0bb88f10 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:10:52 +0800 Subject: [PATCH 094/166] Test the diagram editor's image download checks --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- test/test_utils/test_image_download.py | 133 +++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_image_download.py diff --git a/architecture_explore.md b/architecture_explore.md index cc69a1bb..52ba6d5a 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 141 個 `test_*.py`、2539 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2548 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 087c3fdd..2a65cd21 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -384,3 +384,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: `progress.md` #53 names `qt_material` and asks about declaring `qt-material` too. - **Files**: `progress.md` - **Open items**: `progress.md` #53. + +## U-20260925-43 · 2026-09-25 · Tests of the diagram editor's image download itself · #test #security #diagram + +- **What**: coverage of `diagram_editor/diagram_net_utils.py` showed that the suite never ran `safe_download_image`'s own checks: the scene's tests stand in for the whole function, and `test_public_http.py` reaches it only for a rebinding name. Untested were refusing an unsafe URL as an `ImageDownloadError` (the canvas catches that; an `UnsafeURLError` would escape), refusing a `text/*` page posing as an image, the declared-size cap, the cap on a body whose length is absent or understated, and a malformed or negative `Content-Length`. +- **Change**: tests only: new `test_image_download.py` (9) against a local HTTP server, with 127.0.0.1 allowed to stand in for a public server and every other private address still refused. An image comes back whole; a `text/html` answer is refused naming its type; a declared length over the cap is refused, and so is a longer body with no length (the cap set to 100 bytes for the test); an unsafe URL is refused as an `ImageDownloadError` with nothing requested; a redirect to 169.254.169.254 is not followed, while one on the same public host is; a `Content-Length` of `abc` or `-5` leaves the bounded read to decide. +- **Result / numbers**: `diagram_net_utils.py` at 98% of branches: the one left is `redirect_request` returning `None`, which CPython's handler never does (it raises instead). 2548 tests in 142 files. `ruff check` clean. +- **Files**: `test/test_utils/test_image_download.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b70b06e6..de0c1454 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-43 | 2026-09-25 | Tests of the diagram editor's image download itself | #test #security #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-42 | 2026-09-25 | Every undeclared direct import in progress #53 | #docs #deps | [2026-09-c](2026-09-c.md) | | U-20260925-41 | 2026-09-25 | Record cryptography with the undeclared direct imports in progress #53 | #docs #deps #security | [2026-09-c](2026-09-c.md) | | U-20260925-40 | 2026-09-25 | Tests that reach every branch of the pinned connections and capped reads | #test #security #network | [2026-09-c](2026-09-c.md) | @@ -445,4 +446,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 42 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 43 | diff --git a/test/test_utils/test_image_download.py b/test/test_utils/test_image_download.py new file mode 100644 index 00000000..39b9f858 --- /dev/null +++ b/test/test_utils/test_image_download.py @@ -0,0 +1,133 @@ +"""The diagram editor's image download refuses what is not a public image of a bounded size. + +The scene's tests stand in for ``safe_download_image``, so none of its own +checks ran: a text page posing as an image, a size over the cap (declared or +not), an unsafe URL, a redirect to a private address. +""" +from __future__ import annotations + +import ipaddress +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import pytest + +from pybreeze.pybreeze_ui.diagram_editor import diagram_net_utils +from pybreeze.pybreeze_ui.diagram_editor.diagram_net_utils import ImageDownloadError, safe_download_image +from pybreeze.utils.network import url_validation + +_PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 40 +_LOOPBACK = ipaddress.ip_address("127.0.0.1") + + +class _Server: + """Serves ``routes[path] = (status, headers, body)`` on loopback.""" + + def __init__(self, routes: dict[str, tuple[int, dict[str, str], bytes]]) -> None: + self.requests: list[str] = [] + server = self + + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.0" # a body with no length ends when the connection does + + def do_GET(self): # noqa: N802 — the name http.server calls + server.requests.append(self.path) + status, headers, body = routes[self.path] + self.send_response(status) + for name, value in headers.items(): + self.send_header(name, value) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *_args): + """Quiet.""" + + self._httpd = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + self.port = self._httpd.server_address[1] + self._thread = threading.Thread(target=self._httpd.serve_forever, daemon=True) + self._thread.start() + + def url(self, path: str) -> str: + return f"http://127.0.0.1:{self.port}{path}" + + def close(self) -> None: + self._httpd.shutdown() + self._httpd.server_close() + + +@pytest.fixture() +def loopback_only(monkeypatch): + """127.0.0.1 may stand in for a public server; every other private address stays refused.""" + blocked = url_validation._is_blocked_ip + monkeypatch.setattr(url_validation, "_is_blocked_ip", lambda ip: ip != _LOOPBACK and blocked(ip)) + + +@pytest.fixture() +def serve(): + servers: list[_Server] = [] + + def start(routes): + server = _Server(routes) + servers.append(server) + return server + + yield start + for server in servers: + server.close() + + +def test_an_image_comes_back_whole(loopback_only, serve): + server = serve({"/a.png": (200, {"Content-Type": "image/png", "Content-Length": str(len(_PNG))}, _PNG)}) + assert safe_download_image(server.url("/a.png")) == _PNG + + +def test_a_text_page_is_not_an_image(loopback_only, serve): + server = serve({"/a.png": (200, {"Content-Type": "text/html; charset=utf-8"}, b"not found")}) + with pytest.raises(ImageDownloadError, match="text/html"): + safe_download_image(server.url("/a.png")) + + +def test_a_declared_size_over_the_cap_is_refused_unread(loopback_only, serve, monkeypatch): + monkeypatch.setattr(diagram_net_utils, "MAX_DOWNLOAD_BYTES", 100) + server = serve({"/big.png": (200, {"Content-Type": "image/png", "Content-Length": "1000"}, b"\x00" * 1000)}) + with pytest.raises(ImageDownloadError): + safe_download_image(server.url("/big.png")) + + +def test_a_body_over_the_cap_with_no_length_is_refused(loopback_only, serve, monkeypatch): + # An absent or low Content-Length must not let a larger body through + monkeypatch.setattr(diagram_net_utils, "MAX_DOWNLOAD_BYTES", 100) + server = serve({"/big.png": (200, {"Content-Type": "image/png"}, b"\x00" * 500)}) + with pytest.raises(ImageDownloadError): + safe_download_image(server.url("/big.png")) + + +def test_an_unsafe_url_is_refused_as_a_download_error(serve): + # The canvas catches ImageDownloadError; an UnsafeURLError would escape it + server = serve({"/a.png": (200, {"Content-Type": "image/png"}, _PNG)}) + with pytest.raises(ImageDownloadError): + safe_download_image(server.url("/a.png")) + assert server.requests == [] + + +def test_a_redirect_to_a_private_address_is_not_followed(loopback_only, serve): + server = serve({"/a.png": (302, {"Location": "http://169.254.169.254/latest/meta-data/"}, b"")}) + with pytest.raises(ImageDownloadError): + safe_download_image(server.url("/a.png")) + assert server.requests == ["/a.png"] + + +def test_a_redirect_to_a_public_address_is_followed(loopback_only, serve): + server = serve({ + "/old.png": (301, {"Location": "/new.png"}, b""), + "/new.png": (200, {"Content-Type": "image/png", "Content-Length": str(len(_PNG))}, _PNG), + }) + assert safe_download_image(server.url("/old.png")) == _PNG + assert server.requests == ["/old.png", "/new.png"] + + +@pytest.mark.parametrize("length", ["abc", "-5"]) +def test_a_length_that_is_no_length_leaves_the_read_to_decide(loopback_only, serve, length): + # A hostile or broken header is not trusted either way: the bounded read still caps it + server = serve({"/a.png": (200, {"Content-Type": "image/png", "Content-Length": length}, _PNG)}) + assert safe_download_image(server.url("/a.png")) == _PNG From d9a378b2cfd3885b0223927199f88c1d7a20ce51 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:13:04 +0800 Subject: [PATCH 095/166] Test that stopping a run stops what it started --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- test/test_utils/test_subprocess_util.py | 80 +++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 52ba6d5a..082bfdeb 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2548 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2551 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 2a65cd21..721c02b9 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -392,3 +392,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: `diagram_net_utils.py` at 98% of branches: the one left is `redirect_request` returning `None`, which CPython's handler never does (it raises instead). 2548 tests in 142 files. `ruff check` clean. - **Files**: `test/test_utils/test_image_download.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-44 · 2026-09-25 · Tests that stopping a run stops what it started · #test #executor + +- **What**: `subprocess_util.stop_tree`, which the run window's Stop and the IDE's closing use to end a run with every process it started (`go run`, `cargo run` and a web run start the real program as a grandchild), had no test of its own: nothing checked that a grandchild ends, that an ended process is left alone, or that the child is still terminated when the tree kill fails (`taskkill` missing, `killpg` refused). Coverage of the full suite at `5de7821` (2524 passed, 15 skipped; 91% of statements) showed its fallback lines unrun. +- **Change**: tests only: `TestStoppingATree` in `test_subprocess_util.py` (3). A launcher starts a grandchild that writes a heartbeat file every 50 ms; after `stop_tree` the heartbeat stops (on Windows `os.kill(pid, 0)` would itself terminate the process, so the heartbeat is the portable check; the grandchild also stops itself after 30 s, whatever happens). Run with `stop_tree` replaced by terminating only the launcher, that test fails. An ended process is neither killed nor terminated; with the tree kill refused, the child alone is terminated. +- **Result / numbers**: `subprocess_util.py` covered but for its two POSIX-only lines, which the Windows CI cannot reach. 2551 tests in 142 files. `ruff check` clean. +- **Files**: `test/test_utils/test_subprocess_util.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index de0c1454..63129596 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-44 | 2026-09-25 | Tests that stopping a run stops what it started | #test #executor | [2026-09-c](2026-09-c.md) | | U-20260925-43 | 2026-09-25 | Tests of the diagram editor's image download itself | #test #security #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-42 | 2026-09-25 | Every undeclared direct import in progress #53 | #docs #deps | [2026-09-c](2026-09-c.md) | | U-20260925-41 | 2026-09-25 | Record cryptography with the undeclared direct imports in progress #53 | #docs #deps #security | [2026-09-c](2026-09-c.md) | @@ -446,4 +447,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 43 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 44 | diff --git a/test/test_utils/test_subprocess_util.py b/test/test_utils/test_subprocess_util.py index 647fdcbd..21ec07cc 100644 --- a/test/test_utils/test_subprocess_util.py +++ b/test/test_utils/test_subprocess_util.py @@ -3,6 +3,8 @@ import subprocess import sys +import pytest + from pybreeze.utils.subprocess_util import no_window_creationflags, utf8_subprocess_env @@ -43,3 +45,81 @@ def test_is_a_noop_when_or_combined_on_posix_semantics(self): # OR-ing the flag with other creationflags must never lose existing bits. base = 0x4 assert base | no_window_creationflags() >= base + + +class TestStoppingATree: + """Stopping a run stops what it started: ``go run`` and a web run start the program as a grandchild.""" + + _GRANDCHILD = ( + "import sys, time\n" + "deadline = time.monotonic() + 30\n" + "while time.monotonic() < deadline:\n" + " open(sys.argv[1], 'w').write(str(time.monotonic()))\n" + " time.sleep(0.05)\n" + ) + + def test_the_program_a_launcher_started_stops_too(self, tmp_path): + import time + + from pybreeze.utils.subprocess_util import own_session_options, stop_tree + + heartbeat = tmp_path / "beat" + grandchild = tmp_path / "grandchild.py" + grandchild.write_text(self._GRANDCHILD, encoding="utf-8") + launcher = subprocess.Popen( # noqa: S603 — this interpreter and a script the test wrote + [sys.executable, "-c", + "import subprocess, sys, time\n" + f"subprocess.Popen([sys.executable, {str(grandchild)!r}, {str(heartbeat)!r}])\n" + "time.sleep(30)\n"], + creationflags=no_window_creationflags(), **own_session_options()) + deadline = time.monotonic() + 20 + while not heartbeat.exists() and time.monotonic() < deadline: + time.sleep(0.05) + assert heartbeat.exists(), "the grandchild never started" + + stop_tree(launcher) + launcher.wait(10) + time.sleep(0.5) + last = heartbeat.read_text(encoding="utf-8") + time.sleep(0.5) + + assert heartbeat.read_text(encoding="utf-8") == last, "the grandchild is still running" + + def test_a_process_that_has_ended_is_left_alone(self, monkeypatch): + from pybreeze.utils import subprocess_util + + class Ended: + pid = 1 + + def poll(self): + return 0 + + def terminate(self): + raise AssertionError("an ended process was terminated") + + monkeypatch.setattr(subprocess_util.subprocess, "run", lambda *a, **k: pytest.fail("taskkill was run")) + subprocess_util.stop_tree(Ended()) + + def test_when_the_tree_cannot_be_stopped_the_child_still_is(self, monkeypatch): + from pybreeze.utils import subprocess_util + + class Running: + pid = 4242 + terminated = False + + def poll(self): + return 0 if self.terminated else None + + def terminate(self): + self.terminated = True + + def refuse(*_args, **_kwargs): + raise OSError("taskkill is not there") + + monkeypatch.setattr(subprocess_util.subprocess, "run", refuse) + monkeypatch.setattr(subprocess_util.os, "killpg", refuse, raising=False) + child = Running() + + subprocess_util.stop_tree(child) + + assert child.terminated From 7f14bc071a6e96524b447712e7f828783d06f2d7 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:20:41 +0800 Subject: [PATCH 096/166] Test the host-key store's last line and a closed panel's question --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++ docs/updates/README.md | 3 +- test/test_utils/test_ssh_host_key_policy.py | 48 +++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 082bfdeb..801c1567 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2551 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2554 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 721c02b9..b89c0fc2 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -400,3 +400,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: `subprocess_util.py` covered but for its two POSIX-only lines, which the Windows CI cannot reach. 2551 tests in 142 files. `ruff check` clean. - **Files**: `test/test_utils/test_subprocess_util.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-45 · 2026-09-25 · Tests of the SSH host-key store's last line and a closed panel's question · #test #ssh #security + +- **What**: two behaviours of `connect_gui/ssh/ssh_host_key_policy.py` had no test: trusting a host when `~/.pybreeze/ssh_known_hosts` does not end in a newline (a file edited by hand, say) must not glue the new key onto the last host's line, spoiling both; and a question asked for a panel that was closed before it came is a No, with nobody asked on its behalf. +- **Change**: tests only, in `test_ssh_host_key_policy.py` (3): `test_a_file_without_a_last_newline_keeps_its_last_host` (both hosts still load with their keys), and `TestThePanelThatAsked`: the question carries the panel that asked, and a panel collected before the question makes the connect fail as a No without asking. +- **Result / numbers**: the policy's lines 125 and 151-152, the only ones the full suite did not run, now run. 2554 tests in 142 files. `ruff check` clean. +- **Files**: `test/test_utils/test_ssh_host_key_policy.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 63129596..fd33144f 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-45 | 2026-09-25 | Tests of the SSH host-key store's last line and a closed panel's question | #test #ssh #security | [2026-09-c](2026-09-c.md) | | U-20260925-44 | 2026-09-25 | Tests that stopping a run stops what it started | #test #executor | [2026-09-c](2026-09-c.md) | | U-20260925-43 | 2026-09-25 | Tests of the diagram editor's image download itself | #test #security #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-42 | 2026-09-25 | Every undeclared direct import in progress #53 | #docs #deps | [2026-09-c](2026-09-c.md) | @@ -447,4 +448,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 44 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 45 | diff --git a/test/test_utils/test_ssh_host_key_policy.py b/test/test_utils/test_ssh_host_key_policy.py index 2960d2c6..6a6086f7 100644 --- a/test/test_utils/test_ssh_host_key_policy.py +++ b/test/test_utils/test_ssh_host_key_policy.py @@ -223,3 +223,51 @@ def locked(self): monkeypatch.setattr(Path, "read_bytes", real_read) assert known.read_bytes() == b"trusted.example ssh-ed25519 AAAA\n" + + +def test_a_file_without_a_last_newline_keeps_its_last_host(asked, keys): + # Written straight after it, the new key would join the last line and spoil both + _meet("first.example", keys[0]) + path = policy_mod._known_hosts_path() + path.write_bytes(path.read_bytes().rstrip(b"\n")) + + _meet("second.example", keys[1]) + + known = policy_mod._read_known_hosts() + assert known.lookup("first.example")["ssh-rsa"] == keys[0] + assert known.lookup("second.example")["ssh-rsa"] == keys[1] + + +class TestThePanelThatAsked: + def test_its_question_carries_the_panel(self, asked, keys, monkeypatch): + from PySide6.QtWidgets import QWidget + + parents: list = [] + + class Asker: + def ask(self, parent, _title, _message) -> bool: + parents.append(parent) + return False + + monkeypatch.setattr(policy_mod, "host_key_asker", Asker) + panel = QWidget() + with pytest.raises(paramiko.SSHException): # the answer was No + policy_mod.InteractiveHostKeyPolicy(panel).missing_host_key( + paramiko.SSHClient(), "host.example", keys[0]) + + assert parents == [panel] + panel.deleteLater() + + def test_a_panel_closed_before_the_question_is_a_no(self, asked, keys): + import gc + + from PySide6.QtWidgets import QWidget + + panel = QWidget() + policy = policy_mod.InteractiveHostKeyPolicy(panel) + del panel + gc.collect() + + with pytest.raises(paramiko.SSHException): + policy.missing_host_key(paramiko.SSHClient(), "host.example", keys[0]) + assert asked["count"] == 0 # nobody was asked on its behalf From a848db2d72aca0ef98dec69af41d0ada23d36d8f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:21:00 +0800 Subject: [PATCH 097/166] Let coverage count what QThread workers run --- CLAUDE.md | 2 +- docs/updates/2026-09-c.md | 10 ++++++++++ docs/updates/README.md | 3 ++- test/test_utils/conftest.py | 31 +++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a44ab253..53f00ec4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ pybreeze/ - `unit-tests` job: GitHub Actions on Windows, Python 3.10–3.14 — install deps → pytest `test/test_utils/` → `start_automation_test` → `extend_automation_test` - `sonarcloud` job: CI-based SonarQube Cloud analysis (`sonar-project.properties`), `needs: unit-tests` so it can consume the `coverage-xml` artifact that leg uploads. Automatic Analysis is off and must stay off — the two modes are mutually exclusive and the scanner refuses to run alongside it - SonarCloud's plan for this organization exposes results for `main` and for pull requests only. An analysis pushed for another branch succeeds but its results read back 403, so `dev.yml` scans on pull requests only; `stable.yml` also scans pushes to `main`. Do not "fix" this by scanning every `dev` push — the numbers are not readable -- Coverage comes from the 3.12 matrix leg (`pytest --cov`), configured by `.coveragerc`. `relative_files = True` is required: the report is produced on Windows and consumed by a Linux scanner, so it must not carry machine-specific paths +- Coverage comes from the 3.12 matrix leg (`pytest --cov`), configured by `.coveragerc`. `relative_files = True` is required: the report is produced on Windows and consumed by a Linux scanner, so it must not carry machine-specific paths. coverage traces only the threads Python starts, so `test/test_utils/conftest.py` gives every `QThread` subclass a `run` that installs its tracer on Qt's thread; without it no `QThread.run` counts as covered ## Development diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index b89c0fc2..5e3cac38 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -408,3 +408,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: the policy's lines 125 and 151-152, the only ones the full suite did not run, now run. 2554 tests in 142 files. `ruff check` clean. - **Files**: `test/test_utils/test_ssh_host_key_policy.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-46 · 2026-09-25 · Coverage counts what QThread workers run · #test #ci + +- **What**: coverage.py traces the threads Python starts; a `QThread`'s `run` executes on a thread Qt starts, so every one of the package's twelve `run` methods, and everything they call on their thread, read as never executed, though the tests run them (the SFTP listings, transfers and requests, the regex worker, the diff, the review requests). The SonarCloud numbers, taken from the same measurement, undercounted the same way. coverage.py's own issue for it (#686) has no fix. +- **Change**: `test/test_utils/conftest.py` sets `QThread.__init_subclass__` so that every subclass defined after it (the package's, imported by the tests) gets a `run` that first installs, on its own thread, the tracer `threading` hands the threads Python starts (`threading.gettrace()`). Without coverage there is no tracer, and it does nothing; where coverage uses `sys.monitoring`, which sees every thread, likewise. `CLAUDE.md` › Branching & CI says so beside the coverage settings. +- **Tests**: the full suite passes with it (2539 passed, 15 skipped). The four SFTP test files alone measure `sftp_session.py` at 80% where they measured 61%. +- **Result / numbers**: over the whole suite, 730 of 10,546 statements unrun where 782 were (91% either way, rounded), and 247 partial branches where 253 were. +- **Files**: `test/test_utils/conftest.py`, `CLAUDE.md` +- **Evidence**: [coverage.py issue #686, Coverage missing for QThreads](https://github.com/nedbat/coveragepy/issues/686). +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index fd33144f..22268476 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-46 | 2026-09-25 | Coverage counts what QThread workers run | #test #ci | [2026-09-c](2026-09-c.md) | | U-20260925-45 | 2026-09-25 | Tests of the SSH host-key store's last line and a closed panel's question | #test #ssh #security | [2026-09-c](2026-09-c.md) | | U-20260925-44 | 2026-09-25 | Tests that stopping a run stops what it started | #test #executor | [2026-09-c](2026-09-c.md) | | U-20260925-43 | 2026-09-25 | Tests of the diagram editor's image download itself | #test #security #diagram | [2026-09-c](2026-09-c.md) | @@ -448,4 +449,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 45 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 46 | diff --git a/test/test_utils/conftest.py b/test/test_utils/conftest.py index 107e8ed8..5a333281 100644 --- a/test/test_utils/conftest.py +++ b/test/test_utils/conftest.py @@ -6,11 +6,42 @@ """ from __future__ import annotations +import functools +import sys +import threading + import pytest +from PySide6.QtCore import QThread from pybreeze.pybreeze_ui.gui_thread_gc import GuiThreadGarbageCollector +def _traced_run(run): + """*run*, handing its thread the tracer ``threading`` gives the threads Python starts.""" + @functools.wraps(run) + def traced(self, *args, **kwargs): + tracer = threading.gettrace() + if tracer is not None: + sys.settrace(tracer) + return run(self, *args, **kwargs) + return traced + + +def _measure_qthreads(cls, **kwargs) -> None: + """Let coverage see a ``QThread``'s ``run``: it traces the threads Python starts, not Qt's. + + Every QThread subclass defined after this (the package's are imported by the + tests) gets a ``run`` that installs coverage's tracer on its own thread. It + does nothing when coverage is not running: there is no tracer to install. + """ + super(QThread, cls).__init_subclass__(**kwargs) + if "run" in cls.__dict__: + cls.run = _traced_run(cls.__dict__["run"]) + + +QThread.__init_subclass__ = classmethod(_measure_qthreads) + + @pytest.fixture(scope="session", autouse=True) def _gui_thread_collector(): collector = GuiThreadGarbageCollector() From e156ed1b47a4f1fe1eab6d1fa026179778984d18 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:22:17 +0800 Subject: [PATCH 098/166] Re-measure the coverage the architecture map quotes --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 801c1567..4e77700c 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -465,7 +465,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 - **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2554 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 -- **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 89%(`utils/`、`tools_gui`、`dialog` 97–100%;`extend/` 93%、`connect_gui` 89%、`diagram_editor` 85%、`jupyter_lab_gui` 82%、`menu` 81%;最低的是 `editor_main` 71%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率) +- **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 - **靜態分析** SonarCloud(`sonar-project.properties`,CI-based analysis;Automatic Analysis 已關閉且必須維持關閉,兩種模式互斥)+ Codacy(`.codacy.yml`)+ Bandit(`pyproject.toml` 中排除 test、skip B101/B404) - **SonarCloud 方案限制** 該組織的方案只開放 `main` 與 PR 的分析結果。非 main 分支的分析送得出去、CE 任務也會成功,但結果讀回來是 403(組織內每個專案都只有 `main` 一條分支)。因此 `dev.yml` 只在 PR 時掃描,`stable.yml` 另外掃 push to `main` diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 5e3cac38..8f825631 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -418,3 +418,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/conftest.py`, `CLAUDE.md` - **Evidence**: [coverage.py issue #686, Coverage missing for QThreads](https://github.com/nedbat/coveragepy/issues/686). - **Open items**: none. + +## U-20260925-47 · 2026-09-25 · Re-measure the coverage the architecture map quotes · #docs #test + +- **What**: `architecture_explore.md` §18 gave coverage as 89% overall with per-package figures from an older run, and did not say how `QThread` workers are counted (U-20260925-46). +- **Result / numbers**: measured from the full suite at `7f14bc0` plus the `conftest.py` hook (2539 passed, 15 skipped): statements 93.1% (730 of 10,546 unrun), 91% with branches. By package, statements: `pybreeze_ui/dialog` 100%, `tools_gui` 99.3%, `utils/` 98.1%, `extend_ai_gui` 95.0%, `extend/` 93.9%, `connect_gui` 93.8%, `diagram_editor` 89.7%, `menu` 85.2%, `jupyter_lab_gui` 80.2%, `editor_main` 72.2% (the main window runs mostly in the child-process startup tests, which are not measured; so does `code_result_logs.py`, at 44%). +- **Change**: §18's coverage line gives these figures and says how the `QThread` workers come to be counted. +- **Files**: `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 22268476..171071d0 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-47 | 2026-09-25 | Re-measure the coverage the architecture map quotes | #docs #test | [2026-09-c](2026-09-c.md) | | U-20260925-46 | 2026-09-25 | Coverage counts what QThread workers run | #test #ci | [2026-09-c](2026-09-c.md) | | U-20260925-45 | 2026-09-25 | Tests of the SSH host-key store's last line and a closed panel's question | #test #ssh #security | [2026-09-c](2026-09-c.md) | | U-20260925-44 | 2026-09-25 | Tests that stopping a run stops what it started | #test #executor | [2026-09-c](2026-09-c.md) | @@ -449,4 +450,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 46 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 47 | From 72e3e6868fc92d8f19c8a21e7bdc6d1d7c7251cd Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:24:02 +0800 Subject: [PATCH 099/166] Test a saved diagram's local images --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- test/test_utils/test_diagram_images.py | 28 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 4e77700c..a9571e90 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2554 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2557 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 8f825631..b6bbf8c5 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -426,3 +426,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: §18's coverage line gives these figures and says how the `QThread` workers come to be counted. - **Files**: `architecture_explore.md` - **Open items**: none. + +## U-20260925-48 · 2026-09-25 · Tests of a saved diagram's local images · #test #diagram #security + +- **What**: a `.diagram.json` is untrusted data, and the project's rules let the editor read an image path from it only after checking the extension against an allowlist and that the path is on this machine and a file (`DiagramScene._try_load_image_source`). The refusals of a UNC path had a test (`test_diagram_serialization.py`), but the full suite never ran the load that passes the checks, nor a refused extension or a missing file. +- **Change**: tests only: `TestAnImageOnThisMachine` in `test_diagram_images.py` (3): a PNG next to the test is shown; the same PNG named `.txt` is not read, the extension being checked before the file is touched; a missing file leaves the image empty. None of them is fetched as a URL. +- **Result / numbers**: 2557 tests in 142 files. `ruff check` clean. +- **Files**: `test/test_utils/test_diagram_images.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 171071d0..19077899 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-48 | 2026-09-25 | Tests of a saved diagram's local images | #test #diagram #security | [2026-09-c](2026-09-c.md) | | U-20260925-47 | 2026-09-25 | Re-measure the coverage the architecture map quotes | #docs #test | [2026-09-c](2026-09-c.md) | | U-20260925-46 | 2026-09-25 | Coverage counts what QThread workers run | #test #ci | [2026-09-c](2026-09-c.md) | | U-20260925-45 | 2026-09-25 | Tests of the SSH host-key store's last line and a closed panel's question | #test #ssh #security | [2026-09-c](2026-09-c.md) | @@ -450,4 +451,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 47 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 48 | diff --git a/test/test_utils/test_diagram_images.py b/test/test_utils/test_diagram_images.py index 64eaa852..dc716f78 100644 --- a/test/test_utils/test_diagram_images.py +++ b/test/test_utils/test_diagram_images.py @@ -353,3 +353,31 @@ def test_an_image_resized_down_and_back_is_as_sharp_as_before(app): item.set_size(400, 400) assert item._pix_item.pixmap().toImage() == before + + +class TestAnImageOnThisMachine: + """A saved diagram is untrusted: only an image file here, of a kind it may name, is read.""" + + @staticmethod + def _loaded(app, source: str, downloads: list) -> bool: + scene = DiagramScene() + scene.load_from_dict({"nodes": [], "connections": [], + "images": [{"x": 0, "y": 0, "w": 4, "h": 4, "source": source}]}) + app.processEvents() + loaded = not scene.get_all_images()[0]._pix_item.pixmap().isNull() + assert downloads == [] # a path is never fetched + return loaded + + def test_an_image_file_is_shown(self, app, tmp_path, downloads): + picture = tmp_path / "logo.png" + picture.write_bytes(an_image()) + assert self._loaded(app, str(picture), downloads) + + def test_a_file_of_another_kind_is_not_read(self, app, tmp_path, downloads): + # A PNG it is, but named .txt: the extension is checked before the file is touched + disguised = tmp_path / "notes.txt" + disguised.write_bytes(an_image()) + assert not self._loaded(app, str(disguised), downloads) + + def test_a_file_that_is_not_there_is_left_empty(self, app, tmp_path, downloads): + assert not self._loaded(app, str(tmp_path / "gone.png"), downloads) From 0e692e9bc3a4844ce9f67fa059ababb999408c9d Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:25:20 +0800 Subject: [PATCH 100/166] Let Ctrl+Enter call any action --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- pybreeze/pybreeze_ui/run_shortcut.py | 24 ++++++++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index b6bbf8c5..248cc9fa 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -434,3 +434,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2557 tests in 142 files. `ruff check` clean. - **Files**: `test/test_utils/test_diagram_images.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-49 · 2026-09-25 · Refactor: Ctrl+Enter can call any action · #refactor #ui + +- **What**: `press_on_ctrl_enter` (U-20260925-31) could only press a button; the two tools with a button for each direction (U-20260925-50) need Ctrl+Enter to call a method that picks the direction. +- **Change**: `pybreeze_ui/run_shortcut.py` has `act_on_ctrl_enter(tool, action)`, which gives the tool its two shortcuts calling *action*, and `press_on_ctrl_enter` is that with the button's `click` plus the tooltip. The docstring says to connect a bound method or a button's `click`, as the project's conventions ask. No behaviour changes. +- **Tests**: `test_tool_run_shortcut.py` and `test_closed_panels_are_freed.py` pass (41). +- **Files**: `pybreeze/pybreeze_ui/run_shortcut.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 19077899..732bf783 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-49 | 2026-09-25 | Refactor: Ctrl+Enter can call any action | #refactor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-48 | 2026-09-25 | Tests of a saved diagram's local images | #test #diagram #security | [2026-09-c](2026-09-c.md) | | U-20260925-47 | 2026-09-25 | Re-measure the coverage the architecture map quotes | #docs #test | [2026-09-c](2026-09-c.md) | | U-20260925-46 | 2026-09-25 | Coverage counts what QThread workers run | #test #ci | [2026-09-c](2026-09-c.md) | @@ -451,4 +452,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 48 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 49 | diff --git a/pybreeze/pybreeze_ui/run_shortcut.py b/pybreeze/pybreeze_ui/run_shortcut.py index db6a5038..b45a8f92 100644 --- a/pybreeze/pybreeze_ui/run_shortcut.py +++ b/pybreeze/pybreeze_ui/run_shortcut.py @@ -1,6 +1,8 @@ """Ctrl+Enter runs a tool: its text boxes take Enter as a new line.""" from __future__ import annotations +from collections.abc import Callable + from PySide6.QtCore import Qt from PySide6.QtGui import QKeySequence, QShortcut from PySide6.QtWidgets import QAbstractButton, QWidget @@ -11,15 +13,25 @@ RUN_KEYS_SHOWN = "Ctrl+Enter" -def press_on_ctrl_enter(tool: QWidget, button: QAbstractButton) -> None: - """Let Ctrl+Enter anywhere in *tool* press *button*, as a click would. +def act_on_ctrl_enter(tool: QWidget, action: Callable[[], object]) -> None: + """Let Ctrl+Enter anywhere in *tool* call *action*. - A disabled button (a run still going) is not pressed. The shortcuts are - children of *tool*, so they go with it, and apply only while the focus is - in it: a tool in a dock does not take the keys from the editor. + The shortcuts are children of *tool*, so they go with it, and apply only + while the focus is in it: a tool in a dock does not take the keys from the + editor. Connect a bound method or a button's ``click``: a lambda holding + *tool* would keep it alive. """ for keys in RUN_KEYS: shortcut = QShortcut(QKeySequence(keys), tool) shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) - shortcut.activated.connect(button.click) + shortcut.activated.connect(action) + + +def press_on_ctrl_enter(tool: QWidget, button: QAbstractButton) -> None: + """Let Ctrl+Enter anywhere in *tool* press *button*, as a click would. + + A disabled button (a run still going) is not pressed. The button's tooltip + names the keys. + """ + act_on_ctrl_enter(tool, button.click) button.setToolTip(RUN_KEYS_SHOWN) From 0b37f0f806e0d3d7cda55be41f178d370a974642 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:27:18 +0800 Subject: [PATCH 101/166] Pick the direction from the input on Ctrl+Enter in the two-way tools --- README.md | 4 +-- README/README_zh-CN.md | 4 +-- README/README_zh-TW.md | 4 +-- architecture_explore.md | 6 ++--- docs/updates/2026-09-c.md | 10 +++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_english.py | 2 ++ .../extend_traditional_chinese.py | 2 ++ .../pybreeze_ui/tools_gui/query_json_gui.py | 10 +++++++ .../pybreeze_ui/tools_gui/url_builder_gui.py | 10 +++++++ test/test_utils/test_tool_run_shortcut.py | 26 +++++++++++++++++++ 11 files changed, 71 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e6c8207b..8c14683f 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w ## Built-in Tools -In a tool with one main button, Ctrl+Enter anywhere in it presses that button: its text boxes take Enter as a new line. +In a tool with one main button, Ctrl+Enter anywhere in it presses that button: its text boxes take Enter as a new line. In Query ↔ JSON and the URL parser/builder, which convert both ways, it goes the way the input reads: from JSON when the input is a JSON object. ### cURL Import — a copied request becomes a runnable script @@ -215,7 +215,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 756 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 758 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index e70133e3..a7310be9 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -83,7 +83,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ## 内置工具 -只有一个主要按钮的工具,在工具里任何地方按 Ctrl+Enter 就等于按下那个按钮:文本框里的 Enter 是换行。 +只有一个主要按钮的工具,在工具里任何地方按 Ctrl+Enter 就等于按下那个按钮:文本框里的 Enter 是换行。Query ↔ JSON 与 URL 解析/构建器可以双向转换,Ctrl+Enter 会根据输入决定方向:输入是 JSON 对象时从 JSON 转出。 ### cURL 导入——复制的请求变成可运行的脚本 @@ -215,7 +215,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 756 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 758 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index bdbd6eb8..f78dfd86 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -83,7 +83,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ## 內建工具 -只有一個主要按鈕的工具,在工具裡任何地方按 Ctrl+Enter 就等於按下那顆按鈕:文字框裡的 Enter 是換行。 +只有一個主要按鈕的工具,在工具裡任何地方按 Ctrl+Enter 就等於按下那顆按鈕:文字框裡的 Enter 是換行。Query ↔ JSON 與 URL 解析/組建器可以雙向轉換,Ctrl+Enter 會依輸入決定方向:輸入是 JSON 物件時從 JSON 轉出。 ### cURL 匯入 — 複製下來的請求變成可執行的腳本 @@ -215,7 +215,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 756 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 758 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index a9571e90..02848c97 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -238,7 +238,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 兩個橫向共用機制 - **`tool_tabs.open_tool_tab()`** — 工具之間互相「轉交」:Response Inspector 把狀態碼丟給 HTTP Status、headers 丟給 Header Analyzer、JWT 丟給 JWT Decoder、JSON body 丟給 JSON Format;curl 匯入把 URL 丟給 URL Builder。開新分頁並自動聚焦。 -- **`pybreeze_ui/run_shortcut.press_on_ctrl_enter()`**(不在 `tools_gui/` 裡)— 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發;AI Code Review、CoT Code Review 與 Skill Send 的送出鈕也用它 +- **`pybreeze_ui/run_shortcut.press_on_ctrl_enter()`**(不在 `tools_gui/` 裡)— 只有一個主要動作的工具(cURL、Diff、Hash、Header、JSON Format、JWT、Regex、Response)在工具裡任何地方按 Ctrl+Enter 就等於按那顆鈕(文字框裡的 Enter 是換行);shortcut 是工具的子物件、`WidgetWithChildrenShortcut`,焦點不在工具裡時不會搶走編輯器的按鍵,按鈕停用時(還在跑)不會觸發;AI Code Review、CoT Code Review 與 Skill Send 的送出鈕也用它;雙向的 Query ↔ JSON 與 URL Builder 用 `act_on_ctrl_enter()` 接到 `convert_as_pasted()`,輸入是 JSON 物件就往另一個方向轉 - **`output_actions.OutputActions`** — 統一的「複製 / 在編輯器開啟 / 存檔」三顆按鈕,綁在工具的唯讀輸出 `QTextEdit` 上,輸出也經 `exact_text()` 讀(不讓 U+00A0、U+2028 被改掉);副檔名與檔名可傳 callable 動態決定。存檔經 `replace_text()` 整檔替換,失敗(唯讀資料夾、被鎖住的檔案、磁碟滿)時原檔不動、會跳警告,說出檔名與原因。Qt 的文字元件留不住貼上的 CR,換行一律讀成 LF --- @@ -369,7 +369,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 756 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 758 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2557 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2561 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 248cc9fa..7467fe8d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -442,3 +442,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Tests**: `test_tool_run_shortcut.py` and `test_closed_panels_are_freed.py` pass (41). - **Files**: `pybreeze/pybreeze_ui/run_shortcut.py` - **Open items**: none. + +## U-20260925-50 · 2026-09-25 · Ctrl+Enter in the two-way tools goes the way the input reads · #feature #tools #readme + +- **What**: U-20260925-31 left Query ↔ JSON and the URL parser/builder without Ctrl+Enter, since each has a button for each direction. The input already says which is meant: a JSON object is converted from JSON, anything else to it. +- **Change**: both tools connect Ctrl+Enter (`act_on_ctrl_enter`, U-20260925-49) to a bound `convert_as_pasted`, which clicks the from-JSON button when the input, leading blanks aside, starts with `{`, and the to-JSON button otherwise; a click, so a disabled button is left alone. The buttons' tooltips say when Ctrl+Enter picks them ("Ctrl+Enter, when the input is a JSON object" / "... is not a JSON object"; Ctrl+Enter(輸入是 JSON 物件時)). +- **Tests**: `test_ctrl_enter_goes_the_way_the_input_reads` in `test_tool_run_shortcut.py` (4: each tool, with text and with a JSON object); all failed before the change. With the two tools', language and closed-panel tests, 89 pass; `test_the_readmes_count_the_keys_there_are` caught the two new keys again. +- **Result / numbers**: 2561 tests in 142 files; 758 keys in each dictionary. `ruff check` clean. +- **Docs**: the Built-in Tools paragraph of the three READMEs and their key count; `architecture_explore.md`. +- **Files**: `pybreeze/pybreeze_ui/tools_gui/query_json_gui.py`, `pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_tool_run_shortcut.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 732bf783..136e3283 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-50 | 2026-09-25 | Ctrl+Enter in the two-way tools goes the way the input reads | #feature #tools #readme | [2026-09-c](2026-09-c.md) | | U-20260925-49 | 2026-09-25 | Refactor: Ctrl+Enter can call any action | #refactor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-48 | 2026-09-25 | Tests of a saved diagram's local images | #test #diagram #security | [2026-09-c](2026-09-c.md) | | U-20260925-47 | 2026-09-25 | Re-measure the coverage the architecture map quotes | #docs #test | [2026-09-c](2026-09-c.md) | @@ -452,4 +453,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 49 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 50 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 96dc87c2..625749fc 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -526,6 +526,8 @@ # Query <-> JSON — Widget "query_json_input_label": "Query string or JSON object:", "query_json_input_placeholder": "a=1&b=2 or {\"a\": \"1\", \"b\": \"2\"}", + "ctrl_enter_when_json": "Ctrl+Enter, when the input is a JSON object", + "ctrl_enter_when_not_json": "Ctrl+Enter, when the input is not a JSON object", "query_json_to_json_button": "Query → JSON", "query_json_to_query_button": "JSON → Query", "query_json_output_label": "Result:", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 67b85b38..96a34b70 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -489,6 +489,8 @@ # Query <-> JSON — 介面 "query_json_input_label": "查詢字串或 JSON 物件:", "query_json_input_placeholder": "a=1&b=2 或 {\"a\": \"1\", \"b\": \"2\"}", + "ctrl_enter_when_json": "Ctrl+Enter(輸入是 JSON 物件時)", + "ctrl_enter_when_not_json": "Ctrl+Enter(輸入不是 JSON 物件時)", "query_json_to_json_button": "Query → JSON", "query_json_to_query_button": "JSON → Query", "query_json_output_label": "結果:", diff --git a/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py b/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py index 717770b7..6242cb64 100644 --- a/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py @@ -6,6 +6,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.run_shortcut import act_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.exception.exceptions import QueryConvertException @@ -36,6 +37,10 @@ def __init__(self, main_window=None) -> None: self.to_json_button.clicked.connect(self.convert_to_json) self.to_query_button = QPushButton(word.get("query_json_to_query_button")) self.to_query_button.clicked.connect(self.convert_to_query) + # Ctrl+Enter goes the way the input reads: from JSON for a JSON object + self.to_json_button.setToolTip(word.get("ctrl_enter_when_not_json")) + self.to_query_button.setToolTip(word.get("ctrl_enter_when_json")) + act_on_ctrl_enter(self, self.convert_as_pasted) buttons = QHBoxLayout() buttons.addWidget(self.to_json_button) @@ -59,6 +64,11 @@ def __init__(self, main_window=None) -> None: layout.addLayout(self.actions.button_row()) self.setLayout(layout) + def convert_as_pasted(self) -> None: + """Ctrl+Enter: JSON → query for a JSON object, query → JSON otherwise.""" + pasted_json = exact_text(self.input_edit).lstrip().startswith("{") + (self.to_query_button if pasted_json else self.to_json_button).click() + def convert_to_json(self) -> None: """Convert the input query string to JSON.""" text = exact_text(self.input_edit).strip() diff --git a/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py b/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py index a1ccd76c..5b653db0 100644 --- a/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py @@ -6,6 +6,7 @@ ) from je_editor import language_wrapper +from pybreeze.pybreeze_ui.run_shortcut import act_on_ctrl_enter from pybreeze.pybreeze_ui.exact_text import exact_text from pybreeze.pybreeze_ui.tools_gui.output_actions import OutputActions from pybreeze.utils.exception.exceptions import UrlConvertException @@ -37,6 +38,10 @@ def __init__(self, main_window=None, initial_url: str | None = None) -> None: self.to_json_button.clicked.connect(self.convert_to_json) self.to_url_button = QPushButton(word.get("url_builder_to_url_button")) self.to_url_button.clicked.connect(self.convert_to_url) + # Ctrl+Enter goes the way the input reads: from JSON for a JSON object + self.to_json_button.setToolTip(word.get("ctrl_enter_when_not_json")) + self.to_url_button.setToolTip(word.get("ctrl_enter_when_json")) + act_on_ctrl_enter(self, self.convert_as_pasted) buttons = QHBoxLayout() buttons.addWidget(self.to_json_button) @@ -64,6 +69,11 @@ def __init__(self, main_window=None, initial_url: str | None = None) -> None: self.input_edit.setPlainText(initial_url) self.convert_to_json() + def convert_as_pasted(self) -> None: + """Ctrl+Enter: JSON → URL for a JSON object, URL → JSON otherwise.""" + pasted_json = exact_text(self.input_edit).lstrip().startswith("{") + (self.to_url_button if pasted_json else self.to_json_button).click() + def convert_to_json(self) -> None: """Parse the input URL into its JSON parts.""" word = language_wrapper.language_word_dict diff --git a/test/test_utils/test_tool_run_shortcut.py b/test/test_utils/test_tool_run_shortcut.py index f9cfb05e..1d051e60 100644 --- a/test/test_utils/test_tool_run_shortcut.py +++ b/test/test_utils/test_tool_run_shortcut.py @@ -111,3 +111,29 @@ def test_ctrl_enter_sends_from_an_ai_panel(app, module, name): assert pressed == [True] panel.deleteLater() + + +# The tools with a button for each direction: (module, class, to-JSON button, from-JSON button, text that is not JSON) +TWO_WAY = [ + ("query_json_gui", "QueryJsonGUI", "to_json_button", "to_query_button", "a=1&b=2"), + ("url_builder_gui", "UrlBuilderGUI", "to_json_button", "to_url_button", "https://api.example.com/v1?x=1"), +] + + +@pytest.mark.parametrize(("module", "name", "to_json", "from_json", "text"), TWO_WAY, ids=[t[1] for t in TWO_WAY]) +@pytest.mark.parametrize("pasted", ["text", "json"]) +def test_ctrl_enter_goes_the_way_the_input_reads(app, module, name, to_json, from_json, text, pasted): + tool = _tool(module, name) + pressed: list = [] + getattr(tool, to_json).clicked.connect(lambda: pressed.append(to_json)) + getattr(tool, from_json).clicked.connect(lambda: pressed.append(from_json)) + tool.input_edit.setPlainText(text if pasted == "text" else ' {"a": "1"}') + + shortcuts = _run_shortcuts(tool) + assert len(shortcuts) == 2 + shortcuts[0].activated.emit() + + assert pressed == [to_json if pasted == "text" else from_json] + assert "Ctrl+Enter" in getattr(tool, to_json).toolTip() + assert "Ctrl+Enter" in getattr(tool, from_json).toolTip() + tool.deleteLater() From e3dc209e60a277c0a2e6fb6b2b587ad94ba405a7 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:28:47 +0800 Subject: [PATCH 102/166] Stop the READMEs promising keyword highlighting --- README.md | 4 ++-- README/README_zh-CN.md | 4 ++-- README/README_zh-TW.md | 4 ++-- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8c14683f..1162d0f1 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w ### IDE core -- **Automation-aware syntax highlighting** — the `AT_*` / GUI / Web / Load keyword sets are registered for `.json`, and TestPioneer's schema for `.yml` and `.yaml`, on top of JEditor's language support +- **Automation keyword sets** — the `AT_*` / GUI / Web / Load keyword sets are registered for `.json`, and TestPioneer's schema for `.yml` and `.yaml`, on top of JEditor's language support. JEditor does not colour them yet: it highlights those files with its own rules for the suffix, which leave registered keywords out - **Code editor** — built on [JEditor](https://github.com/Integration-Automation/JEDITOR): tabs, project tree, format checker, debugger, terminal, variable inspector and a git client pane - **Script execution** — single or batch, each run in a window of its own with a Stop button; action files are passed by path, and a script from the tab in front that is too long for a Windows command line (~32 KB) goes through a temporary file - **Report generation** — HTML / JSON / XML after a run, with optional email delivery @@ -331,7 +331,7 @@ start_editor(theme="dark_teal.xml") # any qt_material theme Once launched: -1. **Write** an automation script in the editor — automation keywords highlight as you type +1. **Write** an automation script in the editor 2. **Run** it from the `Automation` menu, picking the target module 3. **Watch** the output stream into the run window 4. **Generate** an HTML / JSON / XML report diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index a7310be9..91bd8a91 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -72,7 +72,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ### IDE 核心 -- **识别自动化的语法高亮** — 在 JEditor 的语言支持之上,为 `.json` 注册了 `AT_*` / GUI / Web / Load 关键字集,为 `.yml` 和 `.yaml` 注册了 TestPioneer 的结构定义 +- **自动化关键字集** — 在 JEditor 的语言支持之上,为 `.json` 注册了 `AT_*` / GUI / Web / Load 关键字集,为 `.yml` 和 `.yaml` 注册了 TestPioneer 的结构定义。JEditor 目前还不会为它们着色:它用自己针对这些扩展名的规则高亮,不会用到注册的关键字 - **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建:标签页、项目树、格式检查、调试器、终端、变量查看器以及 git 客户端面板 - **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮;动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 - **报告生成** — 运行后生成 HTML / JSON / XML 报告,可选以邮件发送 @@ -331,7 +331,7 @@ start_editor(theme="dark_teal.xml") # 任意 qt_material 主题 启动后: -1. **编写** — 在编辑器中编写自动化脚本,自动化关键字会随输入高亮 +1. **编写** — 在编辑器中编写自动化脚本 2. **运行** — 从 `Automation` 菜单运行,选择目标模块 3. **查看** — 输出实时流入运行窗口 4. **生成** — 生成 HTML / JSON / XML 报告 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index f78dfd86..7cd5b8f1 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -72,7 +72,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ### IDE 核心 -- **認得自動化的語法高亮** — 在 JEditor 的語言支援之上,`AT_*`/GUI/Web/Load 關鍵字集註冊給 `.json`,TestPioneer 的結構描述註冊給 `.yml` 與 `.yaml` +- **自動化關鍵字集** — 在 JEditor 的語言支援之上,`AT_*`/GUI/Web/Load 關鍵字集註冊給 `.json`,TestPioneer 的結構描述註冊給 `.yml` 與 `.yaml`。JEditor 目前還不會為它們上色:它用自己針對這些副檔名的規則高亮,不會用到註冊的關鍵字 - **程式碼編輯器** — 以 [JEditor](https://github.com/Integration-Automation/JEDITOR) 為基礎:分頁、專案樹、格式檢查、除錯器、終端機、變數檢視器與 git 用戶端面板 - **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕;動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 - **報告產生** — 執行後產生 HTML/JSON/XML 報告,並可選擇以電子郵件寄送 @@ -331,7 +331,7 @@ start_editor(theme="dark_teal.xml") # 任何 qt_material 主題 啟動後: -1. **撰寫** — 在編輯器中撰寫自動化腳本,自動化關鍵字會隨著輸入高亮 +1. **撰寫** — 在編輯器中撰寫自動化腳本 2. **執行** — 從 `Automation` 選單執行,選擇目標模組 3. **觀看** — 輸出即時傳進執行視窗 4. **產生** — HTML/JSON/XML 報告 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 7467fe8d..9dddadbe 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -452,3 +452,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Docs**: the Built-in Tools paragraph of the three READMEs and their key count; `architecture_explore.md`. - **Files**: `pybreeze/pybreeze_ui/tools_gui/query_json_gui.py`, `pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_tool_run_shortcut.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-51 · 2026-09-25 · The READMEs no longer promise keyword highlighting · #docs #readme #jeditor + +- **What**: the READMEs' feature list headed a line "Automation-aware syntax highlighting", and the Quick Start's first step said "automation keywords highlight as you type". They do not: PyBreeze registers its keyword sets for `.json`, `.yml` and `.yaml`, but JEditor highlights those suffixes with its own rules, which leave registered keywords out (`progress.md` #103, blocked on JEditor). U-20260924-300 corrected the main window's caption for the same reason; these two lines were missed. +- **Change**: in the three READMEs the feature line is "Automation keyword sets" and says JEditor does not colour them yet and why; the Quick Start's first step no longer mentions highlighting. They go back when #103 is fixed. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` +- **Open items**: `progress.md` #103. diff --git a/docs/updates/README.md b/docs/updates/README.md index 136e3283..1f426245 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-51 | 2026-09-25 | The READMEs no longer promise keyword highlighting | #docs #readme #jeditor | [2026-09-c](2026-09-c.md) | | U-20260925-50 | 2026-09-25 | Ctrl+Enter in the two-way tools goes the way the input reads | #feature #tools #readme | [2026-09-c](2026-09-c.md) | | U-20260925-49 | 2026-09-25 | Refactor: Ctrl+Enter can call any action | #refactor #ui | [2026-09-c](2026-09-c.md) | | U-20260925-48 | 2026-09-25 | Tests of a saved diagram's local images | #test #diagram #security | [2026-09-c](2026-09-c.md) | @@ -453,4 +454,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 50 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 51 | From cc770775acda1f3def4186f14ab5347c97bf1cca Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:30:12 +0800 Subject: [PATCH 103/166] Record the build configs left under the old name --- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- progress.md | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 9dddadbe..939ba184 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -459,3 +459,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: in the three READMEs the feature line is "Automation keyword sets" and says JEditor does not colour them yet and why; the Quick Start's first step no longer mentions highlighting. They go back when #103 is fixed. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` - **Open items**: `progress.md` #103. + +## U-20260925-52 · 2026-09-25 · Record the build configs left under the old name as progress #112 · #docs #decision + +- **What**: checking the README's Quick Start against the repository led to `exe/`. The Windows auto-py-to-exe settings build PyBreeze (`exe/start_pybreeze.py`, `exe/pybreeze_icon.ico`, since 987252e), but the Linux settings (`exe/auto_py_to_exe_setting_linux.json`) and the InstallForge project (`exe/automation_ide_setup_config.ifp`) still build AutomationIDE: its name and version 1.0.9, its repository as website and licence link, a script `exe/start_automation_editor.py` and an icon `exe/je_driver_icon.ico` that the repository no longer has, and paths on another build machine (`C:/CodeWorkspace/Python/AutomationIDE`). +- **Change**: `progress.md` #112 [DECIDE]: bring the two over to PyBreeze as the Windows settings were, or delete them if they are no longer used. Their paths are a build machine's, so the owner has to say which. +- **Files**: `progress.md` +- **Open items**: `progress.md` #112. diff --git a/docs/updates/README.md b/docs/updates/README.md index 1f426245..8892cef0 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-52 | 2026-09-25 | Record the build configs left under the old name as progress #112 | #docs #decision | [2026-09-c](2026-09-c.md) | | U-20260925-51 | 2026-09-25 | The READMEs no longer promise keyword highlighting | #docs #readme #jeditor | [2026-09-c](2026-09-c.md) | | U-20260925-50 | 2026-09-25 | Ctrl+Enter in the two-way tools goes the way the input reads | #feature #tools #readme | [2026-09-c](2026-09-c.md) | | U-20260925-49 | 2026-09-25 | Refactor: Ctrl+Enter can call any action | #refactor #ui | [2026-09-c](2026-09-c.md) | @@ -454,4 +455,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 51 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 52 | diff --git a/progress.md b/progress.md index c79429d4..55985060 100644 --- a/progress.md +++ b/progress.md @@ -20,3 +20,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#109** [DECIDE] `je-editor>=1.0.27` (`requirements.txt`, `pyproject.toml`, `dev.toml`) lets an environment keep a gitpython below 3.1.59, which has CVE-2026-78676 (a config write turns a quoted value into a live `core.hooksPath`: code runs on the next hook) and CVE-2026-78679; the IDE's git features use it (JEditor's `je_editor/git_client/`). je-editor 1.0.28 differs from 1.0.27 only in requiring `gitpython>=3.1.59`, and 3.1.59 still has CVE-2026-87818 (the diff API's `--no-index` reads any path), fixed in 3.1.60. Should the dependencies say `je-editor>=1.0.28`, declare `gitpython>=3.1.60`, or both? JEditor's own floor is its to raise (`D:\Codes\JEDITOR\pyproject.toml:18`). - **#110** [BLOCKED] A file opened from the project tree shows as unsaved (" *" on its tab, `_is_modified` true) before anything is typed: JEditor's `EditorWidget.open_an_file` fills the editor with `setPlainText`, which fires `_on_text_changed`, and never clears the mark afterwards (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:295` and `:347-358` in `D:\Codes\JEDITOR` at 1bf81e3; plain `EditorMain` does the same). Needs `open_an_file` to clear the mark (`mark_saved()`) once the file is loaded. - **#111** [DECIDE] AI Code Review, CoT Code Review and Skill Send refuse an endpoint on this machine or on a private network (`validate_url` in `ai_code_review_gui.py:77`, `code_review_thread.py:33`, `skills_send_gui.py:55`; `url_validation._is_blocked_ip`), so a local model server (Ollama, LM Studio, a prthinker server on localhost) cannot be used from them, although CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` until U-20260925-11. The Security › Network rule rejects loopback and private addresses for every user-supplied URL. Should these panels let the user allow a loopback (or private) endpoint, for example after a confirmation naming the address, or stay public-only? +- **#112** [DECIDE] Two of the executable build configs still build the IDE under its old name: `exe/auto_py_to_exe_setting_linux.json` (name AutomationIDE, script `exe/start_automation_editor.py` and icon `exe/je_driver_icon.ico`, neither of which exists any more, all under `C:/CodeWorkspace/Python/AutomationIDE`) and the InstallForge project `exe/automation_ide_setup_config.ifp` (program AutomationIDE 1.0.9, the AutomationIDE repository as its website and licence link, the same icon, `...\AutomationIDE\output\AutomationIDE.exe`). The Windows `exe/auto_py_to_exe_setting.json` builds PyBreeze from `exe/start_pybreeze.py` with `exe/pybreeze_icon.ico` (987252e). Should the two be brought over to PyBreeze the same way (their paths are a build machine's), or deleted if they are no longer used? From 7472876ef51f21159b64ce8530f2ba8330cc2684 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:31:14 +0800 Subject: [PATCH 104/166] Name the root documents in the README's project tree --- README.md | 4 +++- README/README_zh-CN.md | 4 +++- README/README_zh-TW.md | 4 +++- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1162d0f1..edcd6695 100644 --- a/README.md +++ b/README.md @@ -387,10 +387,12 @@ PyBreeze/ │ └── utils/ # curl/HAR parsing, headers, JWT, hashing, │ # URL validation, logging, exceptions, … ├── exe/ # Standalone launcher & build configs -├── docs/ # Sphinx documentation source +├── docs/ # Sphinx documentation source; updates/ is the change log ├── test/ # Unit tests (test_utils) + startup tests ├── images/ # Screenshots +├── architecture.md # Architecture overview: layers, flows, cross-project contracts ├── architecture_explore.md # Module-by-module architecture notes +├── progress.md # Work still to do ├── PLUGIN_GUIDE.md # Plugin development documentation ├── pyproject.toml # Package configuration (stable) ├── dev.toml # Package configuration (dev channel) diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 91bd8a91..905a06a7 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -387,10 +387,12 @@ PyBreeze/ │ └── utils/ # curl/HAR 解析、请求头、JWT、哈希、 │ # URL 验证、日志、异常 …… ├── exe/ # 独立启动器与构建配置 -├── docs/ # Sphinx 文档源码 +├── docs/ # Sphinx 文档源码;updates/ 是更新记录 ├── test/ # 单元测试(test_utils)+ 启动测试 ├── images/ # 截图 +├── architecture.md # 架构总览:分层、主要流程、跨项目约定 ├── architecture_explore.md # 逐个模块的架构说明 +├── progress.md # 尚未完成的工作 ├── PLUGIN_GUIDE.md # 插件开发文档 ├── pyproject.toml # 包配置(稳定版) ├── dev.toml # 包配置(开发通道) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 7cd5b8f1..0264978f 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -387,10 +387,12 @@ PyBreeze/ │ └── utils/ # curl/HAR 解析、標頭、JWT、雜湊、 │ # URL 驗證、日誌、例外…… ├── exe/ # 獨立啟動器與建置設定 -├── docs/ # Sphinx 文件原始碼 +├── docs/ # Sphinx 文件原始碼;updates/ 是更新紀錄 ├── test/ # 單元測試(test_utils)+ 啟動測試 ├── images/ # 截圖 +├── architecture.md # 架構總覽:分層、主要流程、跨專案約定 ├── architecture_explore.md # 逐模組的架構筆記 +├── progress.md # 尚未完成的工作 ├── PLUGIN_GUIDE.md # 外掛開發文件 ├── pyproject.toml # 套件設定(穩定版) ├── dev.toml # 套件設定(開發通道) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 939ba184..6d9aaa4c 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -466,3 +466,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: `progress.md` #112 [DECIDE]: bring the two over to PyBreeze as the Windows settings were, or delete them if they are no longer used. Their paths are a build machine's, so the owner has to say which. - **Files**: `progress.md` - **Open items**: `progress.md` #112. + +## U-20260925-53 · 2026-09-25 · The README's project tree names the root documents · #docs #readme + +- **What**: the READMEs' Project Structure tree listed `architecture_explore.md` and `PLUGIN_GUIDE.md` but not `architecture.md`, the short architecture overview every repository in the workspace keeps (layers, main flows, the cross-project contracts in §6), nor `progress.md`, and described `docs/` as Sphinx source only, though `docs/updates/` holds the change log. +- **Change**: the tree in the three READMEs lists `architecture.md` and `progress.md` and says `docs/updates/` is the change log. Everything else it names was checked to exist. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8892cef0..653f6558 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-53 | 2026-09-25 | The README's project tree names the root documents | #docs #readme | [2026-09-c](2026-09-c.md) | | U-20260925-52 | 2026-09-25 | Record the build configs left under the old name as progress #112 | #docs #decision | [2026-09-c](2026-09-c.md) | | U-20260925-51 | 2026-09-25 | The READMEs no longer promise keyword highlighting | #docs #readme #jeditor | [2026-09-c](2026-09-c.md) | | U-20260925-50 | 2026-09-25 | Ctrl+Enter in the two-way tools goes the way the input reads | #feature #tools #readme | [2026-09-c](2026-09-c.md) | @@ -455,4 +456,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 52 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 53 | From 847f4b44164464e9b2976d6ff8165147fedc43da Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:33:48 +0800 Subject: [PATCH 105/166] Tell which interpreter a run uses, in the README and a docstring --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- pybreeze/extend/process_executor/process_executor_utils.py | 4 +++- 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index edcd6695..013d2636 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w - **Script execution** — single or batch, each run in a window of its own with a Stop button; action files are passed by path, and a script from the tab in front that is too long for a Windows command line (~32 KB) goes through a temporary file - **Report generation** — HTML / JSON / XML after a run, with optional email delivery - **Integrated JupyterLab** — launches as a tab, installing JupyterLab into the project venv if it is missing -- **Virtual environment awareness** — `venv/` and `.venv/` are detected and used automatically; without one, scripts run on the interpreter the IDE runs on +- **Virtual environment awareness** — a run uses the interpreter chosen under **Python Env**; with none chosen, a `venv/` or `.venv/` in the working folder is detected and used, and without one, the interpreter the IDE runs on --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 905a06a7..7a1d7663 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -77,7 +77,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: - **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮;动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 - **报告生成** — 运行后生成 HTML / JSON / XML 报告,可选以邮件发送 - **集成 JupyterLab** — 以标签页方式启动;项目 venv 中没有 JupyterLab 时会自动安装 -- **虚拟环境感知** — 自动检测并使用 `venv/` 与 `.venv/`;没有虚拟环境时,脚本以 IDE 本身所用的解释器运行 +- **虚拟环境感知** — 运行时使用在 **Python Env** 中选择的解释器;没有选择时,自动检测并使用工作文件夹中的 `venv/` 或 `.venv/`,都没有时则以 IDE 本身所用的解释器运行 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 0264978f..9d76734a 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -77,7 +77,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: - **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕;動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 - **報告產生** — 執行後產生 HTML/JSON/XML 報告,並可選擇以電子郵件寄送 - **整合 JupyterLab** — 以分頁方式啟動;專案虛擬環境中沒有 JupyterLab 時會自動安裝 -- **虛擬環境感知** — 自動偵測並使用 `venv/` 與 `.venv/`;都沒有時,腳本會以 IDE 本身使用的直譯器執行 +- **虛擬環境感知** — 執行時使用在 **Python Env** 選擇的直譯器;沒有選擇時,自動偵測並使用工作資料夾中的 `venv/` 或 `.venv/`,都沒有時則以 IDE 本身使用的直譯器執行 --- diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 6d9aaa4c..7ef25c9f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -473,3 +473,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: the tree in the three READMEs lists `architecture.md` and `progress.md` and says `docs/updates/` is the change log. Everything else it names was checked to exist. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` - **Open items**: none. + +## U-20260925-54 · 2026-09-25 · Which interpreter a run uses, told right in the README and a docstring · #docs #readme #executor + +- **What**: the READMEs' "Virtual environment awareness" line said a `venv/` or `.venv/` is used automatically and, without one, the IDE's own interpreter, leaving out that an interpreter chosen under Python Env comes before both (`renew_path`). `build_task_process`'s docstring said the fallback after the venv is `PATH`, which only a packaged build does: from source the IDE's own interpreter is used (`default_interpreter`), `PATH`'s `python3` on Windows often being the Store's stub. `architecture_explore.md` already told it right. +- **Change**: the line in the three READMEs gives the order: the Python Env choice, then a `venv/` or `.venv/` in the working folder, then the IDE's interpreter; the docstring names `default_interpreter` and says only a packaged build looks on `PATH`. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `pybreeze/extend/process_executor/process_executor_utils.py` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 653f6558..1ed185c3 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-54 | 2026-09-25 | Which interpreter a run uses, told right in the README and a docstring | #docs #readme #executor | [2026-09-c](2026-09-c.md) | | U-20260925-53 | 2026-09-25 | The README's project tree names the root documents | #docs #readme | [2026-09-c](2026-09-c.md) | | U-20260925-52 | 2026-09-25 | Record the build configs left under the old name as progress #112 | #docs #decision | [2026-09-c](2026-09-c.md) | | U-20260925-51 | 2026-09-25 | The READMEs no longer promise keyword highlighting | #docs #readme #jeditor | [2026-09-c](2026-09-c.md) | @@ -456,4 +457,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 53 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 54 | diff --git a/pybreeze/extend/process_executor/process_executor_utils.py b/pybreeze/extend/process_executor/process_executor_utils.py index e9e5b097..53c4c711 100644 --- a/pybreeze/extend/process_executor/process_executor_utils.py +++ b/pybreeze/extend/process_executor/process_executor_utils.py @@ -254,7 +254,9 @@ def build_task_process( The run window carries the interpreter chosen in the IDE (the Python environment menu, or the saved setting), so the child runs with that interpreter; only when none was chosen does the manager fall back to a - ``venv`` / ``.venv`` in the working directory, then to ``PATH``. The run + ``venv`` / ``.venv`` in the working directory, then to the IDE's own + interpreter (``default_interpreter``; only a packaged build looks on + ``PATH``). The run window holds the manager (``CodeWindow.runner``), so a caller may drop it. """ code_window = open_run_window(main_window) From 95d6b72c9c0b83166a6a872711dc40a9b20d76b1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:34:20 +0800 Subject: [PATCH 106/166] Say which Python JupyterLab starts in --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 013d2636..024949ae 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w - **Code editor** — built on [JEditor](https://github.com/Integration-Automation/JEDITOR): tabs, project tree, format checker, debugger, terminal, variable inspector and a git client pane - **Script execution** — single or batch, each run in a window of its own with a Stop button; action files are passed by path, and a script from the tab in front that is too long for a Windows command line (~32 KB) goes through a temporary file - **Report generation** — HTML / JSON / XML after a run, with optional email delivery -- **Integrated JupyterLab** — launches as a tab, installing JupyterLab into the project venv if it is missing +- **Integrated JupyterLab** — launches as a tab, in the same interpreter a run would use, installing JupyterLab there if it is missing - **Virtual environment awareness** — a run uses the interpreter chosen under **Python Env**; with none chosen, a `venv/` or `.venv/` in the working folder is detected and used, and without one, the interpreter the IDE runs on --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 7a1d7663..ecfb3543 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -76,7 +76,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: - **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建:标签页、项目树、格式检查、调试器、终端、变量查看器以及 git 客户端面板 - **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮;动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 - **报告生成** — 运行后生成 HTML / JSON / XML 报告,可选以邮件发送 -- **集成 JupyterLab** — 以标签页方式启动;项目 venv 中没有 JupyterLab 时会自动安装 +- **集成 JupyterLab** — 以标签页方式启动,使用与运行脚本相同的解释器;那里没有 JupyterLab 时会自动安装 - **虚拟环境感知** — 运行时使用在 **Python Env** 中选择的解释器;没有选择时,自动检测并使用工作文件夹中的 `venv/` 或 `.venv/`,都没有时则以 IDE 本身所用的解释器运行 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 9d76734a..fec11ded 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -76,7 +76,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: - **程式碼編輯器** — 以 [JEditor](https://github.com/Integration-Automation/JEDITOR) 為基礎:分頁、專案樹、格式檢查、除錯器、終端機、變數檢視器與 git 用戶端面板 - **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕;動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 - **報告產生** — 執行後產生 HTML/JSON/XML 報告,並可選擇以電子郵件寄送 -- **整合 JupyterLab** — 以分頁方式啟動;專案虛擬環境中沒有 JupyterLab 時會自動安裝 +- **整合 JupyterLab** — 以分頁方式啟動,使用與執行腳本相同的直譯器;那裡沒有 JupyterLab 時會自動安裝 - **虛擬環境感知** — 執行時使用在 **Python Env** 選擇的直譯器;沒有選擇時,自動偵測並使用工作資料夾中的 `venv/` 或 `.venv/`,都沒有時則以 IDE 本身使用的直譯器執行 --- diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 7ef25c9f..eb9b034f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -480,3 +480,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: the line in the three READMEs gives the order: the Python Env choice, then a `venv/` or `.venv/` in the working folder, then the IDE's interpreter; the docstring names `default_interpreter` and says only a packaged build looks on `PATH`. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `pybreeze/extend/process_executor/process_executor_utils.py` - **Open items**: none. + +## U-20260925-55 · 2026-09-25 · The README says which Python JupyterLab starts in · #docs #readme + +- **What**: the READMEs said the JupyterLab tab installs JupyterLab "into the project venv" if it is missing. The tab picks its interpreter as a run does (`choose_python`: the one chosen under Python Env, else a `venv`/`.venv` in the working folder, else the IDE's own), so with no venv it installs into the IDE's interpreter or the chosen one. +- **Change**: the line in the three READMEs says it starts in the interpreter a run would use and installs JupyterLab there if missing (U-20260925-54 gives that order). +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 1ed185c3..64c54ead 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-55 | 2026-09-25 | The README says which Python JupyterLab starts in | #docs #readme | [2026-09-c](2026-09-c.md) | | U-20260925-54 | 2026-09-25 | Which interpreter a run uses, told right in the README and a docstring | #docs #readme #executor | [2026-09-c](2026-09-c.md) | | U-20260925-53 | 2026-09-25 | The README's project tree names the root documents | #docs #readme | [2026-09-c](2026-09-c.md) | | U-20260925-52 | 2026-09-25 | Record the build configs left under the old name as progress #112 | #docs #decision | [2026-09-c](2026-09-c.md) | @@ -457,4 +458,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 54 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 55 | From 9716c499c45ee8cdf62ef15ce10157ae8548837f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:35:05 +0800 Subject: [PATCH 107/166] Say what the other languages in the menu do --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 024949ae..f8f45d66 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 758 keys, and a test enforces that parity so a new string can never land in one language only. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 758 keys, and a test enforces that parity so a new string can never land in one language only. The Language menu also lists JEditor's Japanese and Simplified Chinese: picked, JEditor's own menus change and PyBreeze's strings stay in English. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index ecfb3543..e75725fb 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -215,7 +215,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 758 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 758 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。语言菜单还列出 JEditor 的日文与简体中文:选择后 JEditor 自己的菜单会随之改变,PyBreeze 的字符串则保持英文。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index fec11ded..ccf79ba9 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -215,7 +215,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 758 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 758 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。語言選單另外列出 JEditor 的日文與簡體中文:選了之後 JEditor 自己的選單會改變,PyBreeze 的字串則維持英文。其他語言可透過翻譯外掛加入。 --- diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index eb9b034f..fa6585d5 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -487,3 +487,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: the line in the three READMEs says it starts in the interpreter a run would use and installs JupyterLab there if missing (U-20260925-54 gives that order). - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` - **Open items**: none. + +## U-20260925-56 · 2026-09-25 · The README says what the other languages in the menu do · #docs #readme #i18n + +- **What**: the READMEs' Multi-Language UI section listed English and Traditional Chinese, but the Language menu also offers JEditor's 日本語 and 简体中文. Picked, JEditor's own menus change and PyBreeze's strings stay in English (JEditor serves those languages from a copy merged with English; `test_each_registered_language_resolves_every_key` keeps every key resolving). Nothing said so, and the Simplified Chinese README's readers are the likeliest to pick 简体中文. +- **Change**: the section in the three READMEs says so. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 64c54ead..9a57af6d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-56 | 2026-09-25 | The README says what the other languages in the menu do | #docs #readme #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-55 | 2026-09-25 | The README says which Python JupyterLab starts in | #docs #readme | [2026-09-c](2026-09-c.md) | | U-20260925-54 | 2026-09-25 | Which interpreter a run uses, told right in the README and a docstring | #docs #readme #executor | [2026-09-c](2026-09-c.md) | | U-20260925-53 | 2026-09-25 | The README's project tree names the root documents | #docs #readme | [2026-09-c](2026-09-c.md) | @@ -458,4 +459,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 55 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 56 | From 4b6831d9aed247c4020b36b22f613aa059115e32 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:36:13 +0800 Subject: [PATCH 108/166] =?UTF-8?q?Say=20=E5=A4=96=E6=8E=9B=20in=20the=20p?= =?UTF-8?q?lugin=20guide,=20as=20the=20IDE=20does?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLUGIN_GUIDE.md | 8 ++++---- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/PLUGIN_GUIDE.md b/PLUGIN_GUIDE.md index 6c53e1fd..dded46ab 100644 --- a/PLUGIN_GUIDE.md +++ b/PLUGIN_GUIDE.md @@ -1,4 +1,4 @@ -# PyBreeze Plugin Guide / 插件開發指南 +# PyBreeze Plugin Guide / 外掛開發指南 PyBreeze is built on JEditor and uses its plugin system unchanged: the `je_editor.plugins` API (`register_programming_language`, `register_natural_language`, `PLUGIN_RUN_CONFIG`), the @@ -17,13 +17,13 @@ page, so a Java run config on a non-English Windows wants `"encoding": "locale"` --- -PyBreeze 建立在 JEditor 之上,原封不動地使用它的插件系統:`je_editor.plugins` API +PyBreeze 建立在 JEditor 之上,原封不動地使用它的外掛系統:`je_editor.plugins` API (`register_programming_language`、`register_natural_language`、`PLUGIN_RUN_CONFIG`)、 -`jeditor_plugins/` 目錄,以及 *插件 → Plugin Browser* 選單。所以兩個編輯器共用一份指南,放在 JEditor repo: +`jeditor_plugins/` 目錄,以及 *外掛 → 外掛瀏覽器* 選單。所以兩個編輯器共用一份指南,放在 JEditor repo: **→ [JEditor `PLUGIN_GUIDE.md`](https://github.com/Integration-Automation/JEDITOR/blob/main/PLUGIN_GUIDE.md)** -現成的插件(C、C++、Go、Java、Rust 語法高亮與執行設定,以及法文介面翻譯)放在 +現成的外掛(C、C++、Go、Java、Rust 語法高亮與執行設定,以及法文介面翻譯)放在 [IDE_Plugins](https://github.com/Jeffrey-Plugin-Repos/IDE_Plugins)。 PyBreeze 多讀一個 JEditor 不讀的執行設定欄位:`"encoding"`,程式輸出用的編碼(`"cp950"`、`"utf-8"`, diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index fa6585d5..54f53f3f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -494,3 +494,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: the section in the three READMEs says so. - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md` - **Open items**: none. + +## U-20260925-57 · 2026-09-25 · 外掛, not 插件, in the plugin guide · #docs #i18n + +- **What**: the Traditional Chinese half of `PLUGIN_GUIDE.md` called plugins 插件, the Mainland term, four times, and sent the reader to a "*插件 → Plugin Browser*" menu; in the Traditional Chinese IDE that menu is 外掛 and its entry 外掛瀏覽器 (PyBreeze's own words over JEditor's, held by `test_traditional_chinese_uses_taiwan_terms`). The Traditional Chinese README was checked for the same terms and has none (its 終端機, 用戶端 and 媒體類型 are the Taiwan ones). +- **Change**: 外掛 throughout the guide, and the menu named *外掛 → 外掛瀏覽器*. +- **Files**: `PLUGIN_GUIDE.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 9a57af6d..bea166e2 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-57 | 2026-09-25 | 外掛, not 插件, in the plugin guide | #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-56 | 2026-09-25 | The README says what the other languages in the menu do | #docs #readme #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-55 | 2026-09-25 | The README says which Python JupyterLab starts in | #docs #readme | [2026-09-c](2026-09-c.md) | | U-20260925-54 | 2026-09-25 | Which interpreter a run uses, told right in the README and a docstring | #docs #readme #executor | [2026-09-c](2026-09-c.md) | @@ -459,4 +460,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 56 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 57 | From bdae3301a583ec52709073bb09e9737ac2ea0783 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:38:09 +0800 Subject: [PATCH 109/166] Name the menu entries in the Sphinx docs as they now read --- docs/source/Eng/ai_tools.rst | 8 ++++---- docs/source/Eng/menu_automation.rst | 18 +++++++++--------- docs/source/Eng/menu_install.rst | 2 +- docs/source/Eng/menu_tools.rst | 4 ++-- docs/source/Eng/ui_overview.rst | 4 ++-- docs/source/Zh/ai_tools.rst | 6 +++--- docs/source/Zh/menu_automation.rst | 18 +++++++++--------- docs/source/Zh/menu_file_run_text.rst | 10 +++++----- docs/source/Zh/menu_install.rst | 2 +- docs/source/Zh/menu_plugins.rst | 2 +- docs/source/Zh/menu_tools.rst | 4 ++-- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 13 files changed, 48 insertions(+), 40 deletions(-) diff --git a/docs/source/Eng/ai_tools.rst b/docs/source/Eng/ai_tools.rst index 7eb26c1a..4b20a57f 100644 --- a/docs/source/Eng/ai_tools.rst +++ b/docs/source/Eng/ai_tools.rst @@ -5,10 +5,10 @@ PyBreeze integrates several AI-powered tools for code review, prompt engineering and LLM interaction. All AI tools are accessible from the **Tools** menu and can be opened as either tabs or dock widgets. -AI Code-Review Client +AI Code Review Client --------------------- -**Menu:** Tools > AI Code-Review Tab / AI Code-Review Dock +**Menu:** Tools > AI Code Review Tab / AI Code Review Dock A client for sending code to an AI API endpoint for automated code review. @@ -41,7 +41,7 @@ Usage CoT Code Review GUI -------------------- -**Menu:** Tools > AI Code-Review Tab / Dock +**Menu:** Tools > AI Code Review Tab / Dock An advanced code review tool using Chain-of-Thought (CoT) prompting for more structured and detailed reviews. @@ -120,7 +120,7 @@ Pre-built Skill Templates Skills Send GUI --------------- -**Menu:** Tools > Skill Send GUI Tab / Skill Prompt Dock +**Menu:** Tools > Skill Send Tab / Skill Send Dock An interface for sending skill-based prompts to an LLM API and viewing responses. diff --git a/docs/source/Eng/menu_automation.rst b/docs/source/Eng/menu_automation.rst index 1de781c3..566e2628 100644 --- a/docs/source/Eng/menu_automation.rst +++ b/docs/source/Eng/menu_automation.rst @@ -8,7 +8,7 @@ and email automation. Each automation module follows a consistent menu structure: - **RUN** submenu -- Execute scripts (single or multi-file, with or without email reporting) -- **HELP** submenu -- Links to documentation and GitHub repository +- **Help** submenu -- Links to documentation and GitHub repository - **Project** submenu -- Create new project templates - **GUI Tab** -- Open an embedded GUI widget (available for some modules) @@ -36,7 +36,7 @@ RUN Submenu * - **Run Multi AutoControl Script With Send** - Runs multiple scripts and sends the results via email. -HELP Submenu +Help Submenu ^^^^^^^^^^^^ .. list-table:: @@ -109,7 +109,7 @@ RUN Submenu * - **Run Multi APITestka Script With Send** - Runs multiple scripts and sends results via email. -HELP Submenu +Help Submenu ^^^^^^^^^^^^ - **Open APITestka Doc** -- Opens https://apitestka.readthedocs.io/ @@ -149,7 +149,7 @@ RUN Submenu * - **Run Multi WebRunner Script With Send** - Runs multiple scripts and sends results via email. -HELP Submenu +Help Submenu ^^^^^^^^^^^^ - **Open WebRunner Doc** -- Opens https://webrunner.readthedocs.io/ @@ -184,7 +184,7 @@ RUN Submenu * - **Run Multi LoadDensity Script With Send** - Runs multiple scripts and sends results via email. -HELP Submenu +Help Submenu ^^^^^^^^^^^^ - **Open LoadDensity Doc** -- Opens https://loaddensity.readthedocs.io/ @@ -224,7 +224,7 @@ RUN Submenu * - **Run Multi FileAutomation Script With Send** - Runs multiple scripts and sends results via email. -HELP Submenu +Help Submenu ^^^^^^^^^^^^ - **Open FileAutomation Doc** -- Opens https://fileautomation.readthedocs.io/ @@ -253,7 +253,7 @@ RUN Submenu * - **Run MailThunder Script** - Executes the current editor content as a MailThunder script. -HELP Submenu +Help Submenu ^^^^^^^^^^^^ - **Open MailThunder Doc** -- Opens https://mailthunder.readthedocs.io/ @@ -275,9 +275,9 @@ TestPioneer Menu * - Menu Item - Description - * - **Create TestPioneer Yaml Template** + * - **Create TestPioneer YAML Template** - Generates a YAML template file for defining test cases. - * - **Execute Test Pioneer Yaml** + * - **Run TestPioneer YAML** - Opens a file dialog to select a ``.yml`` file and executes the test definitions within it. diff --git a/docs/source/Eng/menu_install.rst b/docs/source/Eng/menu_install.rst index c1d141dc..9d206aca 100644 --- a/docs/source/Eng/menu_install.rst +++ b/docs/source/Eng/menu_install.rst @@ -23,7 +23,7 @@ Installs the automation module packages via pip. - Runs ``pip install -U je_load_density`` * - **Install WebRunner** - Runs ``pip install -U je_web_runner`` - * - **Install Automation File** + * - **Install FileAutomation** - Runs ``pip install -U automation_file`` * - **Install MailThunder** - Runs ``pip install -U je_mail_thunder`` diff --git a/docs/source/Eng/menu_tools.rst b/docs/source/Eng/menu_tools.rst index 23445050..bea7922d 100644 --- a/docs/source/Eng/menu_tools.rst +++ b/docs/source/Eng/menu_tools.rst @@ -23,7 +23,7 @@ around the edges of the main window. AI Tools -------- -AI Code-Review Tab / Dock +AI Code Review Tab / Dock ^^^^^^^^^^^^^^^^^^^^^^^^^^ Opens the AI Code Review client, which allows you to send code to an AI API @@ -41,7 +41,7 @@ Skill Prompt Editor Tab / Dock Opens the Skill-based Prompt Editor for creating task-specific prompt templates (e.g., code review prompts, code explanation prompts). See :doc:`ai_tools` for full details. -Skill Send GUI Tab / Dock +Skill Send Tab / Dock ^^^^^^^^^^^^^^^^^^^^^^^^^^ Opens the Skill Prompt Sender interface for sending prompts to an LLM API diff --git a/docs/source/Eng/ui_overview.rst b/docs/source/Eng/ui_overview.rst index e791c581..a17d326a 100644 --- a/docs/source/Eng/ui_overview.rst +++ b/docs/source/Eng/ui_overview.rst @@ -109,10 +109,10 @@ Several tools can be opened as **dock widgets** instead of tabs, allowing you to arrange them freely around the main window: - SSH Client Dock -- AI Code-Review Dock +- AI Code Review Dock - CoT Prompt Editor Dock - Skill Prompt Editor Dock -- Skill Send GUI Dock +- Skill Send Dock - Diagram Editor Dock Dock widgets can be dragged, resized, and snapped to any edge of the main window. diff --git a/docs/source/Zh/ai_tools.rst b/docs/source/Zh/ai_tools.rst index 81b6d346..063a708f 100644 --- a/docs/source/Zh/ai_tools.rst +++ b/docs/source/Zh/ai_tools.rst @@ -7,7 +7,7 @@ PyBreeze 整合了多個 AI 驅動的工具,用於程式碼審查、提示詞 AI 程式碼審查用戶端 -------------------- -**選單:** Tools > AI Code-Review Tab / AI Code-Review Dock +**選單:** Tools > AI Code Review Tab / AI Code Review Dock 用於將程式碼發送到 AI API 端點進行自動程式碼審查的用戶端。 @@ -40,7 +40,7 @@ AI 程式碼審查用戶端 CoT 程式碼審查 GUI -------------------- -**選單:** Tools > AI Code-Review Tab / Dock +**選單:** Tools > AI Code Review Tab / Dock 使用思維鏈(CoT)提示詞進行更結構化和詳細審查的進階程式碼審查工具。 @@ -118,7 +118,7 @@ Skill 提示詞編輯器 Skills 傳送 GUI ---------------- -**選單:** Tools > Skill Send GUI Tab / Skill Prompt Dock +**選單:** Tools > Skill Send Tab / Skill Send Dock 用於將基於技能的提示詞發送到 LLM API 並查看回應的介面。 diff --git a/docs/source/Zh/menu_automation.rst b/docs/source/Zh/menu_automation.rst index 2027a179..59b8ff9d 100644 --- a/docs/source/Zh/menu_automation.rst +++ b/docs/source/Zh/menu_automation.rst @@ -7,7 +7,7 @@ Automation 選單 每個自動化模組都遵循一致的選單結構: - **RUN** 子選單 -- 執行腳本(單檔或多檔,可選擇是否以郵件發送結果) -- **HELP** 子選單 -- 連結到文件和 GitHub 儲存庫 +- **Help** 子選單 -- 連結到文件和 GitHub 儲存庫 - **Project** 子選單 -- 建立新的專案範本 - **GUI Tab** -- 開啟嵌入式 GUI 元件(部分模組可用) @@ -35,7 +35,7 @@ RUN 子選單 * - **Run Multi AutoControl Script With Send** - 執行多個腳本並透過郵件發送結果。 -HELP 子選單 +Help 子選單 ^^^^^^^^^^^ .. list-table:: @@ -106,7 +106,7 @@ RUN 子選單 * - **Run Multi APITestka Script With Send** - 執行多個腳本並透過郵件發送結果。 -HELP 子選單 +Help 子選單 ^^^^^^^^^^^ - **Open APITestka Doc** -- 開啟 https://apitestka.readthedocs.io/ @@ -146,7 +146,7 @@ RUN 子選單 * - **Run Multi WebRunner Script With Send** - 執行多個腳本並透過郵件發送結果。 -HELP 子選單 +Help 子選單 ^^^^^^^^^^^ - **Open WebRunner Doc** -- 開啟 https://webrunner.readthedocs.io/ @@ -180,7 +180,7 @@ RUN 子選單 * - **Run Multi LoadDensity Script With Send** - 執行多個腳本並透過郵件發送結果。 -HELP 子選單 +Help 子選單 ^^^^^^^^^^^ - **Open LoadDensity Doc** -- 開啟 https://loaddensity.readthedocs.io/ @@ -220,7 +220,7 @@ RUN 子選單 * - **Run Multi FileAutomation Script With Send** - 執行多個腳本並透過郵件發送結果。 -HELP 子選單 +Help 子選單 ^^^^^^^^^^^ - **Open FileAutomation Doc** -- 開啟 https://fileautomation.readthedocs.io/ @@ -248,7 +248,7 @@ RUN 子選單 * - **Run MailThunder Script** - 將目前編輯器內容作為 MailThunder 腳本執行。 -HELP 子選單 +Help 子選單 ^^^^^^^^^^^ - **Open MailThunder Doc** -- 開啟 https://mailthunder.readthedocs.io/ @@ -270,9 +270,9 @@ TestPioneer 選單 * - 選單項目 - 說明 - * - **Create TestPioneer Yaml Template** + * - **Create TestPioneer YAML Template** - 產生用於定義測試案例的 YAML 範本檔案。 - * - **Execute Test Pioneer Yaml** + * - **Run TestPioneer YAML** - 開啟檔案對話框選擇 ``.yml`` 檔案,並執行其中定義的測試。 腳本執行流程 diff --git a/docs/source/Zh/menu_file_run_text.rst b/docs/source/Zh/menu_file_run_text.rst index 1bbe3dc0..7161b1c5 100644 --- a/docs/source/Zh/menu_file_run_text.rst +++ b/docs/source/Zh/menu_file_run_text.rst @@ -17,7 +17,7 @@ File 選單 * - **Save File** - 將目前程式碼編輯區的內容儲存到檔案。 * - **Encoding** - - 開啟對話框選擇程式運行器和 Shell 運行器的編碼 + - 開啟對話框選擇程式執行器和 Shell 執行器的編碼 (如 UTF-8、ASCII、Big5)。 Run 選單 @@ -30,7 +30,7 @@ Run 選單 * - 選單項目 - 說明 * - **Run Program** - - 使用程式運行器(Python 直譯器)執行目前程式碼編輯區的內容。 + - 使用程式執行器(Python 直譯器)執行目前程式碼編輯區的內容。 * - **Run On Shell** - 使用系統 Shell 執行目前程式碼編輯區的內容。 * - **Clean Result** @@ -48,9 +48,9 @@ Run Help 子選單 * - 選單項目 - 說明 * - **Run Help** - - 顯示程式運行器的說明資訊。 + - 顯示程式執行器的說明資訊。 * - **Shell Help** - - 顯示 Shell 運行器的說明資訊。 + - 顯示 Shell 執行器的說明資訊。 Text 選單 --------- @@ -90,7 +90,7 @@ Venv 選單 * - 選單項目 - 說明 * - **Create Venv** - - 在目前工作目錄中使用 Shell 運行器建立 Python 虛擬環境。 + - 在目前工作目錄中使用 Shell 執行器建立 Python 虛擬環境。 * - **pip upgrade package** - 在虛擬環境中使用 pip 升級指定的套件。 * - **pip package** diff --git a/docs/source/Zh/menu_install.rst b/docs/source/Zh/menu_install.rst index 45d70526..21956d0a 100644 --- a/docs/source/Zh/menu_install.rst +++ b/docs/source/Zh/menu_install.rst @@ -23,7 +23,7 @@ Automation 子選單 - 執行 ``pip install -U je_load_density`` * - **Install WebRunner** - 執行 ``pip install -U je_web_runner`` - * - **Install Automation File** + * - **Install FileAutomation** - 執行 ``pip install -U automation_file`` * - **Install MailThunder** - 執行 ``pip install -U je_mail_thunder`` diff --git a/docs/source/Zh/menu_plugins.rst b/docs/source/Zh/menu_plugins.rst index 8b6d4531..cd3aa0ea 100644 --- a/docs/source/Zh/menu_plugins.rst +++ b/docs/source/Zh/menu_plugins.rst @@ -43,7 +43,7 @@ Run With 選單 關於建立自訂外掛的詳細資訊,包括語法高亮外掛和 UI 翻譯外掛, 請參閱 `外掛指南 `_ -(指南放在 JEditor repo,因為 PyBreeze 用的是 JEditor 的插件系統)。 +(指南放在 JEditor repo,因為 PyBreeze 用的是 JEditor 的外掛系統)。 語法高亮外掛範例 ^^^^^^^^^^^^^^^^ diff --git a/docs/source/Zh/menu_tools.rst b/docs/source/Zh/menu_tools.rst index 969e8f50..f16916c4 100644 --- a/docs/source/Zh/menu_tools.rst +++ b/docs/source/Zh/menu_tools.rst @@ -20,7 +20,7 @@ SSH Client Dock AI 工具 ------- -AI Code-Review Tab / Dock +AI Code Review Tab / Dock ^^^^^^^^^^^^^^^^^^^^^^^^^^ 開啟 AI 程式碼審查用戶端,允許您將程式碼發送到 AI API 端點進行自動程式碼審查。 @@ -38,7 +38,7 @@ Skill Prompt Editor Tab / Dock 開啟基於技能的提示詞編輯器,用於建立特定任務的提示詞範本 (如程式碼審查提示詞、程式碼解釋提示詞)。詳細資訊請參閱 :doc:`ai_tools`。 -Skill Send GUI Tab / Dock +Skill Send Tab / Dock ^^^^^^^^^^^^^^^^^^^^^^^^^^ 開啟技能提示詞傳送介面,用於將提示詞發送到 LLM API 並查看回應。 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 54f53f3f..21d42672 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -501,3 +501,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: 外掛 throughout the guide, and the menu named *外掛 → 外掛瀏覽器*. - **Files**: `PLUGIN_GUIDE.md` - **Open items**: none. + +## U-20260925-58 · 2026-09-25 · The Sphinx docs name the menu entries as they now read · #docs #i18n + +- **What**: the Sphinx documentation (`docs/source/Eng` and `docs/source/Zh`, published on Read the Docs) quoted the menu labels U-20260925-20 to -22 and U-20260925-11 renamed: "AI Code-Review", "Skill Send GUI Tab / Skill Prompt Dock", the automation menus' "HELP" submenu, "Create TestPioneer Yaml Template", "Execute Test Pioneer Yaml", "Install Automation File". The Chinese pages also used 運行 (運行器) and 插件 in their own prose, where the Traditional Chinese IDE says 執行 and 外掛. +- **Change**: the labels as the IDE shows them (AI Code Review, Skill Send Tab / Skill Send Dock, Help, Create TestPioneer YAML Template, Run TestPioneer YAML, Install FileAutomation) in both languages' pages, and 執行器, 外掛 in the Chinese prose. Section underlines stay long enough: no title grew. +- **Files**: `docs/source/Eng/ai_tools.rst`, `menu_automation.rst`, `menu_install.rst`, `menu_tools.rst`, `ui_overview.rst`; `docs/source/Zh/ai_tools.rst`, `menu_automation.rst`, `menu_file_run_text.rst`, `menu_install.rst`, `menu_plugins.rst`, `menu_tools.rst` +- **Open items**: none; a page-by-page comparison with the menus follows. diff --git a/docs/updates/README.md b/docs/updates/README.md index bea166e2..c9a1fe09 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-58 | 2026-09-25 | The Sphinx docs name the menu entries as they now read | #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-57 | 2026-09-25 | 外掛, not 插件, in the plugin guide | #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-56 | 2026-09-25 | The README says what the other languages in the menu do | #docs #readme #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-55 | 2026-09-25 | The README says which Python JupyterLab starts in | #docs #readme | [2026-09-c](2026-09-c.md) | @@ -460,4 +461,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 57 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 58 | From cb22a1958a89ad80a593dc3d98232b09097b7e64 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:42:45 +0800 Subject: [PATCH 110/166] Show today's main window in the Sphinx docs --- docs/source/Eng/images/ui.png | Bin 63718 -> 91220 bytes docs/source/Zh/images/ui.png | Bin 63718 -> 91220 bytes docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/Eng/images/ui.png b/docs/source/Eng/images/ui.png index dab23198af1f3a6f854cda7c8b50d34a9b24d619..13d261695541eb066b07d442020e277ca69f9d76 100644 GIT binary patch literal 91220 zcmce7WmH_d;9i^%)gNj6e1ONa~rA~DnwAgPze)_L#kh?$;h)n;!6LPe(z!vi5KMy|> z(A59uDAA-32KRp(*eH4ohX=Sv5^ghynYVWFR`yvHF1 z`b~nTjhHTTw`I^^;o#2JgL+ibw+r+{C%tVBkVn(&=(6U9M-;ADwgc3v?TDnb|=rurb0;` z{`uSZ#f1z_(9_EeA@2xNupwW3hxeJgxP}bqt#{euSlDoOewQ?5ivoqOCmo|czOUh9n1R6ZQ>&!@Pwmfa;ciT5$!sTsBb$NcsMWRu9^_*CJuy`M=Ku_XZ z@>7dbXtJ!#DqFtRw>dd<$43zuD80k}iJ6us#W3h)&noW#pBrD9I@~tlhdue5GwlT@o1w7Bb>Z zrd@c&8Vd2_BJK83{#>6(`A25`8qF}cfD=*YDV5rHJF=P#TBrIznxLz>>3-ynn}akP zjt44yxn$dc*(8(wPN-`+HlGh+p}5S?Mn&_nZ(E}uBe#JBO7`~lAlj$-DxH>-_qrk| zmdgpETu$JVFmzFH%UiI1jpK6h#S~udf-N^>X*|06$#|W@dZ&tAit0d*jSk22VY$Ri za>;euzC9|+uz}tOZOTe>mD8(l^d%y(mH4-(qN<#w6C)N1QgZ%} zOXLsf^fH6kBPD(@@s9#TpuORSjmhUfLQ#JeSrbJ{`P1&`O@H~x zg;3Dm$}sRnBRo2>Z+U1` zOx)pg+YIk|V2i^O8lZmVbmhl+qRA6;On;-@U9Q}^|5Kk=ilC?W^SQ%H05Rqz_T*It zi^;3&)&e{l(X+*#3LcBGeyepuT^NJK?nR~qN1FUxL*F)NBKK))YQMwd_-G%Ii8OHV zU*`H+t6yWC@6gIP8z%q^IU(@ znGlWc8qmU?ipOt*1CQfQ4S4ee1I(W0$MfP}bc=rP7;X^u8H3zk70-=@V+dm9OCC2~ z*q=PlAxpkfih7*uUsz{{HQMOZ#edS%y;i0|Wb{0E4iWV@f5lWfE*2IP01@%Q{c=PL zGP1*JYoO8uRFw|Plf&7Gp<9~Xl}O+g72wyvujjVUrrjaCEA~jsWsbJ3n(^_WxQ+gY zTQ_#vyG(7aOVhBRaYo+&@1v9kQiqlH!?9s=D`k==x8a|I`pwQt>I~TDXhD8&V57fh zeFmC0Ha?wyX$TmcFzUGLyxC(*x9D|v!F#H|OuO_`Y{Q+G zMHj(;`VBvrKqSx{bUuX_Va@e@Qsjf**1_hQV2kMQmV_+Z;f})Wb@t8xxU1}}Ic~e< z&xc!I%Hm0*Aj9hSHT7sXsl*jS>yWkXB6(4o)DGg|1G^>`}065V%3CLB@%O0Ck z_)fADwf|dR>PpM|?nl>9=^`~c>=fDnkBhC6Jge8~BCmD3g}OTZu2M_IobT)&JWfjx z<@nBPH{z20B%Y64nG$-tu_I^Fb-gtemC|jqTsS9q9*2U8iim{d%IvTa99Se1f4|c- z$!q^yRo<(Za~Y68vKEnmztm}w)poV7@_00Al}kdULU4DuxF3l^!hiIV?yx-hjaPK& z{!?6Tb2Bm?>!nbh6Y`kWY195#T!oP!3yXHHev$9Z>7*4m&VI8WzAi*QCKr z>p%LjIBqDFd@eCxZG?x1XHewp(WcmsghRmP?LNNjL0z6HAt@uJl*3cxGs;u5A`*DH zQ(K%(J>Jt?#`{h@IDq|n10{u!A=t(72(%cPR+qYN zKGLs5>EfpCZ@bZxR5aM^xL~?jNUf%(My;2WRpJBW@pV7;y*+6&XmNB}ZRTfSXcP~V zLC&L|^ci>=-gzJo8^=}1x+M(*4+|-X9IC74tON z%fT1s!?Hf7RjFwM&R&v*|U{Sm#@c_V`|n9o%Ge&vZu=Q-Hrx}Zp^

    Yc;LH9k1VFd{||4HmO)9Y}2Szq}9;y<}bhRx2> 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

    aQE%Y$y#tU0jXDr`{BSN;c61G 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 NRMxzguVnG+zX8KraLE7w literal 63718 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 csK;kPuMC#tHHV!Z diff --git a/docs/source/Zh/images/ui.png b/docs/source/Zh/images/ui.png index dab23198af1f3a6f854cda7c8b50d34a9b24d619..13d261695541eb066b07d442020e277ca69f9d76 100644 GIT binary patch literal 91220 zcmce7WmH_d;9i^%)gNj6e1ONa~rA~DnwAgPze)_L#kh?$;h)n;!6LPe(z!vi5KMy|> z(A59uDAA-32KRp(*eH4ohX=Sv5^ghynYVWFR`yvHF1 z`b~nTjhHTTw`I^^;o#2JgL+ibw+r+{C%tVBkVn(&=(6U9M-;ADwgc3v?TDnb|=rurb0;` z{`uSZ#f1z_(9_EeA@2xNupwW3hxeJgxP}bqt#{euSlDoOewQ?5ivoqOCmo|czOUh9n1R6ZQ>&!@Pwmfa;ciT5$!sTsBb$NcsMWRu9^_*CJuy`M=Ku_XZ z@>7dbXtJ!#DqFtRw>dd<$43zuD80k}iJ6us#W3h)&noW#pBrD9I@~tlhdue5GwlT@o1w7Bb>Z zrd@c&8Vd2_BJK83{#>6(`A25`8qF}cfD=*YDV5rHJF=P#TBrIznxLz>>3-ynn}akP zjt44yxn$dc*(8(wPN-`+HlGh+p}5S?Mn&_nZ(E}uBe#JBO7`~lAlj$-DxH>-_qrk| zmdgpETu$JVFmzFH%UiI1jpK6h#S~udf-N^>X*|06$#|W@dZ&tAit0d*jSk22VY$Ri za>;euzC9|+uz}tOZOTe>mD8(l^d%y(mH4-(qN<#w6C)N1QgZ%} zOXLsf^fH6kBPD(@@s9#TpuORSjmhUfLQ#JeSrbJ{`P1&`O@H~x zg;3Dm$}sRnBRo2>Z+U1` zOx)pg+YIk|V2i^O8lZmVbmhl+qRA6;On;-@U9Q}^|5Kk=ilC?W^SQ%H05Rqz_T*It zi^;3&)&e{l(X+*#3LcBGeyepuT^NJK?nR~qN1FUxL*F)NBKK))YQMwd_-G%Ii8OHV zU*`H+t6yWC@6gIP8z%q^IU(@ znGlWc8qmU?ipOt*1CQfQ4S4ee1I(W0$MfP}bc=rP7;X^u8H3zk70-=@V+dm9OCC2~ z*q=PlAxpkfih7*uUsz{{HQMOZ#edS%y;i0|Wb{0E4iWV@f5lWfE*2IP01@%Q{c=PL zGP1*JYoO8uRFw|Plf&7Gp<9~Xl}O+g72wyvujjVUrrjaCEA~jsWsbJ3n(^_WxQ+gY zTQ_#vyG(7aOVhBRaYo+&@1v9kQiqlH!?9s=D`k==x8a|I`pwQt>I~TDXhD8&V57fh zeFmC0Ha?wyX$TmcFzUGLyxC(*x9D|v!F#H|OuO_`Y{Q+G zMHj(;`VBvrKqSx{bUuX_Va@e@Qsjf**1_hQV2kMQmV_+Z;f})Wb@t8xxU1}}Ic~e< z&xc!I%Hm0*Aj9hSHT7sXsl*jS>yWkXB6(4o)DGg|1G^>`}065V%3CLB@%O0Ck z_)fADwf|dR>PpM|?nl>9=^`~c>=fDnkBhC6Jge8~BCmD3g}OTZu2M_IobT)&JWfjx z<@nBPH{z20B%Y64nG$-tu_I^Fb-gtemC|jqTsS9q9*2U8iim{d%IvTa99Se1f4|c- z$!q^yRo<(Za~Y68vKEnmztm}w)poV7@_00Al}kdULU4DuxF3l^!hiIV?yx-hjaPK& z{!?6Tb2Bm?>!nbh6Y`kWY195#T!oP!3yXHHev$9Z>7*4m&VI8WzAi*QCKr z>p%LjIBqDFd@eCxZG?x1XHewp(WcmsghRmP?LNNjL0z6HAt@uJl*3cxGs;u5A`*DH zQ(K%(J>Jt?#`{h@IDq|n10{u!A=t(72(%cPR+qYN zKGLs5>EfpCZ@bZxR5aM^xL~?jNUf%(My;2WRpJBW@pV7;y*+6&XmNB}ZRTfSXcP~V zLC&L|^ci>=-gzJo8^=}1x+M(*4+|-X9IC74tON z%fT1s!?Hf7RjFwM&R&v*|U{Sm#@c_V`|n9o%Ge&vZu=Q-Hrx}Zp^

    Yc;LH9k1VFd{||4HmO)9Y}2Szq}9;y<}bhRx2> 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

    aQE%Y$y#tU0jXDr`{BSN;c61G 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 NRMxzguVnG+zX8KraLE7w literal 63718 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 csK;kPuMC#tHHV!Z diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 21d42672..e46931d3 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -508,3 +508,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: the labels as the IDE shows them (AI Code Review, Skill Send Tab / Skill Send Dock, Help, Create TestPioneer YAML Template, Run TestPioneer YAML, Install FileAutomation) in both languages' pages, and 執行器, 外掛 in the Chinese prose. Section underlines stay long enough: no title grew. - **Files**: `docs/source/Eng/ai_tools.rst`, `menu_automation.rst`, `menu_install.rst`, `menu_tools.rst`, `ui_overview.rst`; `docs/source/Zh/ai_tools.rst`, `menu_automation.rst`, `menu_file_run_text.rst`, `menu_install.rst`, `menu_plugins.rst`, `menu_tools.rst` - **Open items**: none; a page-by-page comparison with the menus follows. + +## U-20260925-59 · 2026-09-25 · The Sphinx docs show today's main window · #docs + +- **What**: the UI Overview page of the Sphinx docs, in both languages, showed `images/ui.png`: a 2023 capture of the IDE as "Automation Editor", with a menu bar from before the Tools, AI, Dock, Plugins and Help menus, and a project tree listing that machine's files (`crede…`, `token…` JSON among them). +- **Change**: both pages' `ui.png` is the README's main window (`images/main_window.png`, redone in U-20260924-310 from a demo project), 1600×950. +- **Files**: `docs/source/Eng/images/ui.png`, `docs/source/Zh/images/ui.png` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index c9a1fe09..e8d2179d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-59 | 2026-09-25 | The Sphinx docs show today's main window | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-58 | 2026-09-25 | The Sphinx docs name the menu entries as they now read | #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-57 | 2026-09-25 | 外掛, not 插件, in the plugin guide | #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-56 | 2026-09-25 | The README says what the other languages in the menu do | #docs #readme #i18n | [2026-09-c](2026-09-c.md) | @@ -461,4 +462,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 58 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 59 | From ce5d436a7e75e25675e94e3bdee8cfb069bd17c8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:43:29 +0800 Subject: [PATCH 111/166] Update architecture.md's recipe for a new tool --- architecture.md | 7 +++++-- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/architecture.md b/architecture.md index 48cc4e37..a531e29c 100644 --- a/architecture.md +++ b/architecture.md @@ -120,9 +120,12 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() - add keywords in `pybreeze_ui/syntax/syntax_keyword.py`. - **New tool tab or dock**: a widget in `pybreeze_ui/tools_gui/`, its logic in `pybreeze/utils/`, and rows in `_WIDGET_FACTORIES` / `_TAB_ACTIONS` / `_DOCK_ACTIONS` / `_DOCK_TITLES` - (`pybreeze_ui/menu/tools/tools_menu.py`). + (`pybreeze_ui/menu/tools/tools_menu.py`). Like the other tools, a box that holds code calls + `fixed_pitch.use_fixed_pitch_font()`, and the main button gets Ctrl+Enter through + `run_shortcut.press_on_ctrl_enter()` (`act_on_ctrl_enter()` when the input decides the action). - **UI strings**: add keys to both `extend_multi_language/extend_english.py` and - `extend_traditional_chinese.py`. `test/test_utils/test_language_parity.py` enforces parity. + `extend_traditional_chinese.py`. `test/test_utils/test_language_parity.py` enforces parity, and the + key count in the READMEs and `architecture_explore.md` must follow (`test_the_readmes_count_the_keys_there_are`). ## 6. Cross-project boundaries diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index e46931d3..d8bdcaa9 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -515,3 +515,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: both pages' `ui.png` is the README's main window (`images/main_window.png`, redone in U-20260924-310 from a demo project), 1600×950. - **Files**: `docs/source/Eng/images/ui.png`, `docs/source/Zh/images/ui.png` - **Open items**: none. + +## U-20260925-60 · 2026-09-25 · architecture.md's recipe for a new tool follows today's conventions · #docs + +- **What**: `architecture.md`'s extension points told how to add a tool tab or dock (a widget, its logic in `utils/`, the rows in `tools_menu.py`) but not what every tool now does: code boxes in the fixed-pitch font (U-20260924-314, U-20260925-10) and Ctrl+Enter on the main button (U-20260925-31, -50); and for UI strings, that the key count quoted in the READMEs and `architecture_explore.md` is checked (U-20260925-28, which has caught two changes since). +- **Change**: the New tool tab or dock point names `use_fixed_pitch_font()` and `press_on_ctrl_enter()` / `act_on_ctrl_enter()`; the UI strings point names the key-count test. +- **Files**: `architecture.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index e8d2179d..73cbb3fe 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-60 | 2026-09-25 | architecture.md's recipe for a new tool follows today's conventions | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-59 | 2026-09-25 | The Sphinx docs show today's main window | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-58 | 2026-09-25 | The Sphinx docs name the menu entries as they now read | #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-57 | 2026-09-25 | 外掛, not 插件, in the plugin guide | #docs #i18n | [2026-09-c](2026-09-c.md) | @@ -462,4 +463,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 59 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 60 | From b5de66722f08055bacdf944414ead3f214325457 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:54:04 +0800 Subject: [PATCH 112/166] Render bold and code in the Chinese Sphinx pages --- architecture_explore.md | 2 +- docs/source/Zh/ai_tools.rst | 20 ++++++++--------- docs/source/Zh/getting_started.rst | 2 +- docs/source/Zh/how_to_extend_ui.rst | 2 +- docs/source/Zh/menu_tools.rst | 2 +- docs/source/Zh/ssh_client.rst | 6 ++--- docs/source/Zh/ui_overview.rst | 4 ++-- docs/source/conf.py | 4 ---- docs/updates/2026-09-c.md | 10 +++++++++ docs/updates/README.md | 3 ++- test/test_utils/test_docs_markup.py | 35 +++++++++++++++++++++++++++++ 11 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 test/test_utils/test_docs_markup.py diff --git a/architecture_explore.md b/architecture_explore.md index 02848c97..5b4c06c8 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 142 個 `test_*.py`、2561 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2587 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/source/Zh/ai_tools.rst b/docs/source/Zh/ai_tools.rst index 063a708f..617027f4 100644 --- a/docs/source/Zh/ai_tools.rst +++ b/docs/source/Zh/ai_tools.rst @@ -16,8 +16,8 @@ AI 程式碼審查用戶端 - **URL 輸入框** -- 輸入 API 端點 URL - **方法選擇器** -- 選擇 HTTP 方法(GET、POST、PUT、DELETE) -- **程式碼輸入區**(左面板)-- 貼上或撰寫要審查的程式碼 -- **回應顯示區**(右面板,唯讀)-- 顯示 AI 審查回應 +- **程式碼輸入區**\ (左面板)-- 貼上或撰寫要審查的程式碼 +- **回應顯示區**\ (右面板,唯讀)-- 顯示 AI 審查回應 - **Send Request** 按鈕 -- 將程式碼發送到 API 端點 功能 @@ -49,8 +49,8 @@ CoT 程式碼審查 GUI - **API URL 輸入框** -- 輸入 API 端點 URL - **程式碼區域** -- 貼上要審查的程式碼 -- **回應選擇器**(ComboBox)-- 瀏覽多個審查回應 -- **回應檢視器**(唯讀)-- 顯示選定的審查回應 +- **回應選擇器**\ (ComboBox)-- 瀏覽多個審查回應 +- **回應檢視器**\ (唯讀)-- 顯示選定的審查回應 - **Send** 按鈕 -- 發送程式碼進行審查 功能 @@ -70,8 +70,8 @@ CoT 提示詞編輯器 介面配置 ^^^^^^^^ -- **檔案選擇器**(ComboBox)-- 從可用的提示詞範本檔案中選擇 -- **編輯面板**(QTextEdit)-- 編輯選定的提示詞範本 +- **檔案選擇器**\ (ComboBox)-- 從可用的提示詞範本檔案中選擇 +- **編輯面板**\ (QTextEdit)-- 編輯選定的提示詞範本 - **Create** 按鈕 -- 建立新的提示詞範本檔案 - **Save** 按鈕 -- 儲存目前範本的變更 - **Reload** 按鈕 -- 從磁碟重新載入範本 @@ -103,8 +103,8 @@ Skill 提示詞編輯器 介面配置 ^^^^^^^^ -- **檔案選擇器**(ComboBox)-- 從可用的技能提示詞範本中選擇 -- **編輯面板**(QTextEdit)-- 編輯選定的技能提示詞 +- **檔案選擇器**\ (ComboBox)-- 從可用的技能提示詞範本中選擇 +- **編輯面板**\ (QTextEdit)-- 編輯選定的技能提示詞 - **Create** 按鈕 -- 建立新的技能提示詞範本 - **Save** 按鈕 -- 儲存變更 - **Reload** 按鈕 -- 從磁碟重新載入 @@ -126,10 +126,10 @@ Skills 傳送 GUI ^^^^^^^^ - **API URL 輸入框** -- 輸入 LLM API 端點 URL -- **提示詞範本選擇器**(ComboBox)-- 選擇預定義的技能提示詞範本 +- **提示詞範本選擇器**\ (ComboBox)-- 選擇預定義的技能提示詞範本 - **提示詞文字區域** -- 在發送前編輯或自訂提示詞 - **Send** 按鈕 -- 將提示詞發送到 API(在背景執行緒中執行) -- **回應顯示區**(唯讀)-- 顯示 LLM 回應 +- **回應顯示區**\ (唯讀)-- 顯示 LLM 回應 功能 ^^^^ diff --git a/docs/source/Zh/getting_started.rst b/docs/source/Zh/getting_started.rst index 24745833..5c03a8c1 100644 --- a/docs/source/Zh/getting_started.rst +++ b/docs/source/Zh/getting_started.rst @@ -70,7 +70,7 @@ PyBreeze 啟動後,主視窗會以最大化方式開啟,包含: 1. **選單列** -- 位於頂部,包含所有可用選單 2. **檔案樹** -- 位於左側,用於專案導覽 -3. **程式碼編輯器**(分頁式)-- 位於中央,用於編輯檔案 +3. **程式碼編輯器**\ (分頁式)-- 位於中央,用於編輯檔案 4. **輸出面板** -- 位於底部,顯示執行結果 PyBreeze 繼承了 **JEditor** 的核心編輯器功能,並擴充了自動化專用的選單、工具和整合功能。 diff --git a/docs/source/Zh/how_to_extend_ui.rst b/docs/source/Zh/how_to_extend_ui.rst index 92a76b93..0f1796ba 100644 --- a/docs/source/Zh/how_to_extend_ui.rst +++ b/docs/source/Zh/how_to_extend_ui.rst @@ -42,7 +42,7 @@ PyBreeze 支援使用 ``EDITOR_EXTEND_TAB`` 字典擴充 UI 自訂分頁。 -------- 1. 從 ``pybreeze`` 匯入 ``EDITOR_EXTEND_TAB``。 -2. 建立繼承 ``QWidget``(或任何 QWidget 子類別)的類別。 +2. 建立繼承 ``QWidget``\ (或任何 QWidget 子類別)的類別。 3. 將您的元件類別加入 ``EDITOR_EXTEND_TAB`` 字典,以顯示名稱作為鍵值。 4. 呼叫 ``start_editor()`` -- 您的分頁將與預設分頁一起顯示。 diff --git a/docs/source/Zh/menu_tools.rst b/docs/source/Zh/menu_tools.rst index f16916c4..26963759 100644 --- a/docs/source/Zh/menu_tools.rst +++ b/docs/source/Zh/menu_tools.rst @@ -2,7 +2,7 @@ Tools 選單 ========== **Tools** 選單提供 SSH 用戶端、AI 開發工具,以及內建的 WYSIWYG 架構圖編輯器。 -每個工具都可以作為 **分頁**(在主分頁元件中)或 **停靠面板**(浮動/可停靠面板)開啟。 +每個工具都可以作為 **分頁**\ (在主分頁元件中)或 **停靠面板**\ (浮動/可停靠面板)開啟。 SSH --- diff --git a/docs/source/Zh/ssh_client.rst b/docs/source/Zh/ssh_client.rst index cf6880cf..dda50db1 100644 --- a/docs/source/Zh/ssh_client.rst +++ b/docs/source/Zh/ssh_client.rst @@ -9,9 +9,9 @@ PyBreeze 內建 SSH 用戶端,用於連接遠端伺服器。 SSH 用戶端提供三個主要元件,以水平分割器排列: -1. **登入元件**(頂部)-- 連線設定 -2. **檔案樹**(左側,約 30% 寬度)-- 遠端檔案瀏覽器 -3. **命令元件**(右側,約 70% 寬度)-- 互動式 SSH 終端機 +1. **登入元件**\ (頂部)-- 連線設定 +2. **檔案樹**\ (左側,約 30% 寬度)-- 遠端檔案瀏覽器 +3. **命令元件**\ (右側,約 70% 寬度)-- 互動式 SSH 終端機 登入元件 -------- diff --git a/docs/source/Zh/ui_overview.rst b/docs/source/Zh/ui_overview.rst index 57725502..af3aa2f6 100644 --- a/docs/source/Zh/ui_overview.rst +++ b/docs/source/Zh/ui_overview.rst @@ -119,7 +119,7 @@ SSH、AI 工具、外掛等選單。 PyBreeze 使用 `qt_material `_ 進行主題設定。 可用主題包括: -- ``dark_amber.xml``(預設) +- ``dark_amber.xml``\ (預設) - ``dark_teal.xml`` - ``dark_blue.xml`` - ``dark_cyan.xml`` @@ -148,7 +148,7 @@ PyBreeze 使用 `qt_material `_ 進行 PyBreeze 支援多種 UI 語言: -- **English**(預設) +- **English**\ (預設) - **繁體中文** - 可透過外掛新增其他語言 diff --git a/docs/source/conf.py b/docs/source/conf.py index 711e2c16..8812240e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,8 +26,6 @@ extensions = [] -templates_path = ["_templates"] - language = "en" exclude_patterns = [] @@ -35,5 +33,3 @@ # -- Options for HTML output ------------------------------------------------- html_theme = "sphinx_rtd_theme" - -html_static_path = ["_static"] diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index d8bdcaa9..758c5d94 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -522,3 +522,13 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: the New tool tab or dock point names `use_fixed_pitch_font()` and `press_on_ctrl_enter()` / `act_on_ctrl_enter()`; the UI strings point names the key-count test. - **Files**: `architecture.md` - **Open items**: none. + +## U-20260925-61 · 2026-09-25 · Bold and code in the Chinese Sphinx pages render, and the docs build without warnings · #fix #docs #i18n + +- **What**: building the Sphinx docs gave 20 warnings. 19 were in the Chinese pages: reStructuredText ends `**strong**` or ``literal`` only before whitespace or certain punctuation (not a Chinese character, nor an opening bracket such as () and starts one only after them, and Chinese runs on without spaces, so `**程式碼輸入區**(左面板)` and five other pages' markup were published with the asterisks showing (the built page read `程式碼輸入區**(左面板)`). The last was `conf.py`'s `html_static_path = ["_static"]`, a folder that does not exist, as `templates_path`'s `_templates` does not either. +- **Fix**: an escaped space (backslash, space), which renders as nothing, between the markup and the Chinese next to it, in `Zh/ai_tools.rst`, `getting_started.rst`, `how_to_extend_ui.rst`, `menu_tools.rst`, `ssh_client.rst` and `ui_overview.rst`; the two settings are gone from `conf.py`. `sphinx-build -E` now reports no warning, and the page shows `程式碼輸入區`. +- **Tests**: new `test_docs_markup.py` (26: one per page, and that there are pages) parses each `.rst` with docutils and fails on an inline-markup warning; the six Chinese pages failed before the fix. It skips where docutils is missing; CI installs it with Sphinx from `dev_requirements.txt`. +- **Result / numbers**: 2587 tests in 143 files. `ruff check` clean. +- **Files**: the six `docs/source/Zh/*.rst` pages, `docs/source/conf.py`, `test/test_utils/test_docs_markup.py`, `architecture_explore.md` +- **Evidence**: [Docutils: reStructuredText inline markup recognition rules](https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#inline-markup-recognition-rules). +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 73cbb3fe..17345ba7 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-61 | 2026-09-25 | Bold and code in the Chinese Sphinx pages render, and the docs build without warnings | #fix #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-60 | 2026-09-25 | architecture.md's recipe for a new tool follows today's conventions | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-59 | 2026-09-25 | The Sphinx docs show today's main window | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-58 | 2026-09-25 | The Sphinx docs name the menu entries as they now read | #docs #i18n | [2026-09-c](2026-09-c.md) | @@ -463,4 +464,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 60 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 61 | diff --git a/test/test_utils/test_docs_markup.py b/test/test_utils/test_docs_markup.py new file mode 100644 index 00000000..b3a7e9be --- /dev/null +++ b/test/test_utils/test_docs_markup.py @@ -0,0 +1,35 @@ +"""The Sphinx pages' inline markup renders, in Chinese as in English. + +reStructuredText ends ``**strong**`` or ````literal```` only before whitespace or +certain punctuation, and starts one only after them. Chinese runs on without +spaces, so ``**程式碼輸入區**(左面板)`` was published with its asterisks showing. +An escaped space (backslash, space) between them renders as nothing. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +docutils_core = pytest.importorskip("docutils.core") +from docutils import nodes # noqa: E402 — docutils may be missing: skipped then + +_SOURCE = Path(__file__).resolve().parents[2] / "docs" / "source" + + +def _inline_markup_problems(path: Path) -> list[str]: + document = docutils_core.publish_doctree( + path.read_text(encoding="utf-8"), source_path=str(path), + settings_overrides={"report_level": 5, "halt_level": 5, "warning_stream": False}) + # Only the inline markup: the Sphinx directives (toctree) are unknown to plain docutils + return [message.astext() for message in document.findall(nodes.system_message) + if ") Inline " in message.astext()] + + +def test_there_are_pages_to_check(): + assert len(list(_SOURCE.rglob("*.rst"))) > 20 + + +@pytest.mark.parametrize("page", sorted(_SOURCE.rglob("*.rst")), ids=lambda page: str(page.relative_to(_SOURCE))) +def test_the_inline_markup_of_each_page_closes(page): + assert _inline_markup_problems(page) == [] From 501e82e644de299e45938f4e71967cd8954759fc Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:58:09 +0800 Subject: [PATCH 113/166] Run JupyterLab in the interpreter a run uses --- architecture_explore.md | 4 +- docs/updates/2026-09-c.md | 9 +++ docs/updates/README.md | 3 +- .../jupyter_lab_gui/jupyter_lab_thread.py | 55 ++++++------------- test/test_utils/test_jupyter_lifecycle.py | 2 +- test/test_utils/test_jupyter_ready.py | 51 ++++++++++++++--- 6 files changed, 73 insertions(+), 51 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 5b4c06c8..e53fcf63 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -324,7 +324,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 10. `pybreeze_ui/jupyter_lab_gui/` -- `jupyter_lab_thread.py` — `JupyterLauncherThread(QThread)`:`find_free_port()`(綁 127.0.0.1 讓核心挑空 port)→ `choose_python()`(IDE 選定的直譯器優先,其次 venv 的,最後 IDE 自己的)→ `is_jupyter_installed()`(問直譯器 `find_spec('jupyterlab')`,不問 pip;缺就自動裝)→ 啟動 server(`_start_server()`,在 `_process_lock` 裡先看 `_stopped`:`stop()` 之後就不再啟動,即使還在安裝)→ `_wait_until_ready()` 輪詢 port(60 秒 timeout)→ emit `server_ready(url)`。server 的輸出寫進暫存檔而不是管線(server 起來後沒人讀管線,緩衝區滿了它會卡在 `write()`);提早結束時錯誤訊息取這個檔案的尾巴。失敗時 `error_occurred` 送的是原因(例外訊息,最多 2,000 字),traceback 只進 log;已經 `stop()`(分頁關了)的失敗不算失敗,只記 debug +- `jupyter_lab_thread.py` — `JupyterLauncherThread(QThread)`:`find_free_port()`(綁 127.0.0.1 讓核心挑空 port)→ `choose_python()`(IDE 選定的直譯器優先,否則和執行一樣走 `default_interpreter()`:工作目錄的 `venv`/`.venv`,再來 IDE 自己的;打包版找 PATH,找不到的 `JEditorExecException` 在分頁上顯示原因)→ `is_jupyter_installed()`(問直譯器 `find_spec('jupyterlab')`,不問 pip;缺就自動裝)→ 啟動 server(`_start_server()`,在 `_process_lock` 裡先看 `_stopped`:`stop()` 之後就不再啟動,即使還在安裝)→ `_wait_until_ready()` 輪詢 port(60 秒 timeout)→ emit `server_ready(url)`。server 的輸出寫進暫存檔而不是管線(server 起來後沒人讀管線,緩衝區滿了它會卡在 `write()`);提早結束時錯誤訊息取這個檔案的尾巴。失敗時 `error_occurred` 送的是原因(例外訊息,最多 2,000 字),traceback 只進 log;已經 `stop()`(分頁關了)的失敗不算失敗,只記 debug - `jupyter_lab_widget.py` — 設 `WA_DeleteOnClose`:分頁關閉就刪掉(`close_tab` 只移除分頁、不刪 widget,網頁檢視與它的 Chromium renderer 會一直留到 IDE 結束)。收到 URL 後用 `QWebEngineView.setUrl()` 載入;失敗時在狀態列顯示「初始化失敗:原因」(純文字、可選取、自動換行);`closeEvent` 一律關掉 server(launcher 執行緒在 lab 載入完就結束了,只停「還在跑的執行緒」等於從不停 server);還在安裝或啟動的 launcher 交給 `let_run_out()`,不在 UI 執行緒等(安裝可能要好幾分鐘),也不用 `blockSignals`(那會連 `finished` 一起擋掉,keeper 永遠放不掉它)。IDE 關閉時 `PyBreezeMainWindow._close_tool_tabs_and_docks()` 會關掉所有非編輯器分頁與 `DestroyDock`,這個 `closeEvent` 才會被呼叫到 安全前提(CLAUDE.md 已明列):server 只綁 localhost,因此 token/password 刻意留空、`disable_check_xsrf=True` 才能內嵌。`--ServerApp.port_retries=0`:port 被占就直接結束(走「提早結束」的回報),不讓它默默換 port。**不設 `allow_origin`**:loopback 擋不住瀏覽器,開放來源的話使用者逛到的任何網頁都能操作這個沒有 token 的 server。 @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2587 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2589 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 758c5d94..ed54036f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -532,3 +532,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: the six `docs/source/Zh/*.rst` pages, `docs/source/conf.py`, `test/test_utils/test_docs_markup.py`, `architecture_explore.md` - **Evidence**: [Docutils: reStructuredText inline markup recognition rules](https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#inline-markup-recognition-rules). - **Open items**: none. + +## U-20260925-62 · 2026-09-25 · JupyterLab runs in the interpreter a run uses · #fix #jupyter + +- **What**: with no interpreter chosen under Python Env, a run used a `venv`/`.venv` in the working folder, else the IDE's own (`default_interpreter`), but the JupyterLab tab first asked whether the IDE itself ran in a virtual environment (`get_venv_python`) and used that one if so. An IDE started from a venv of its own therefore ran notebooks there while runs of the same project used the project's `.venv`, so the notebooks did not see the project's packages. `default_interpreter`'s docstring even said the tab chose as it did. Found by the Sphinx docs audit, whose JupyterLab page states the tab's order; U-20260925-55 had written the README's line as if the two already agreed. +- **Fix**: `choose_python` (`jupyter_lab_gui/jupyter_lab_thread.py`) returns the interpreter chosen in the IDE, else `default_interpreter()`, as a run does. That can raise `JEditorExecException` in a packaged build with no Python, which the launcher thread now catches and shows as the tab's failure reason instead of dying with nothing shown. `get_venv_python`, unused after this, is removed, with the `os` and `sys` imports it alone needed. +- **Tests**: in `test_jupyter_ready.py`, `test_the_lab_runs_where_a_run_would` (the IDE in a venv, the project with a `.venv`: the lab gets the project's, as `default_interpreter` gives; it got the IDE's before) and `test_a_packaged_build_with_no_python_says_so` are new; the tests that stood in for `get_venv_python` stand in for `default_interpreter`. The JupyterLab tests pass (27). +- **Result / numbers**: 2589 tests in 143 files. `ruff check` clean. +- **Files**: `pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py`, `test/test_utils/test_jupyter_ready.py`, `test/test_utils/test_jupyter_lifecycle.py`, `architecture_explore.md` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 17345ba7..74a17f92 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-62 | 2026-09-25 | JupyterLab runs in the interpreter a run uses | #fix #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-61 | 2026-09-25 | Bold and code in the Chinese Sphinx pages render, and the docs build without warnings | #fix #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-60 | 2026-09-25 | architecture.md's recipe for a new tool follows today's conventions | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-59 | 2026-09-25 | The Sphinx docs show today's main window | #docs | [2026-09-c](2026-09-c.md) | @@ -464,4 +465,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 61 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 62 | diff --git a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py index b2850e7d..03eefe43 100644 --- a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py +++ b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py @@ -1,17 +1,16 @@ from __future__ import annotations -import os import socket import subprocess -import sys import tempfile import threading import time import traceback from PySide6.QtCore import QThread, Signal -from je_editor import language_wrapper +from je_editor import JEditorExecException, language_wrapper +from pybreeze.extend.process_executor.python_task_process_manager import default_interpreter from pybreeze.utils.logging.logger import pybreeze_logger from pybreeze.utils.subprocess_util import no_window_creationflags @@ -32,43 +31,20 @@ def find_free_port() -> int: return s.getsockname()[1] -def get_venv_python() -> str: - # If already in a venv - if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): - return sys.executable - - # Try common venv locations - if sys.platform in ["win32", "cygwin", "msys"]: - possible_paths = [ - os.path.join(os.getcwd(), "venv", "Scripts", "python.exe"), - os.path.join(os.getcwd(), ".venv", "Scripts", "python.exe"), - ] - else: - possible_paths = [ - os.path.join(os.getcwd(), "venv", "bin", "python"), - os.path.join(os.getcwd(), ".venv", "bin", "python"), - ] - - for path in possible_paths: - if os.path.exists(path): - return path - - raise RuntimeError("Cannot find venv python executable") - - def choose_python(chosen: str | None) -> str: - """The interpreter the lab runs in: the one chosen in the IDE, else a venv's, else the IDE's own. + """The interpreter the lab runs in: the one chosen in the IDE, else the one a run uses. It took the IDE's own or a ``venv``/``.venv`` in the working directory and never the one chosen in the IDE, so kernels ran in the wrong environment, - and an IDE installed outside a venv could not start the lab at all. + and an IDE installed outside a venv could not start the lab at all. Then an + IDE started from a venv of its own ran the lab there, while a run of the + project used the project's ``.venv``: now both go through + ``default_interpreter`` (a ``venv``/``.venv`` in the working directory, + else the IDE's own; a packaged build looks on ``PATH``). + + :raises JEditorExecException: when a packaged build finds no Python """ - if chosen: - return chosen - try: - return get_venv_python() - except RuntimeError: - return sys.executable + return chosen or default_interpreter() def is_jupyter_installed(python_exe: str) -> bool: @@ -115,8 +91,8 @@ def run(self): if not is_jupyter_installed(python_exe): self.status_update.emit(language_wrapper.language_word_dict.get("jupyterlab_downloading")) - # Install jupyterlab into the local venv. python_exe comes from - # get_venv_python(); shell=False. nosec B603. + # Install jupyterlab into the interpreter the lab runs in + # (choose_python); shell=False. nosec B603. result = subprocess.run([ # nosec B603 # nosemgrep # noqa: S603 python_exe, "-m", @@ -143,8 +119,9 @@ def run(self): self._wait_until_ready(port) self.server_ready.emit(f"http://localhost:{port}/lab") - # OSError includes the TimeoutError of a server that never came up - except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error: + # OSError includes the TimeoutError of a server that never came up; + # JEditorExecException, a packaged build that found no Python + except (OSError, ValueError, RuntimeError, subprocess.SubprocessError, JEditorExecException) as error: if self._stopped.is_set(): # The tab closed: stop() ended the server, and the wait saw it # exit. Not a failure, and it used to be logged as one. diff --git a/test/test_utils/test_jupyter_lifecycle.py b/test/test_utils/test_jupyter_lifecycle.py index 36c3687f..b4ddb465 100644 --- a/test/test_utils/test_jupyter_lifecycle.py +++ b/test/test_utils/test_jupyter_lifecycle.py @@ -54,7 +54,7 @@ def popen(argv, **options): started.append(server) return server - monkeypatch.setattr(jupyter_lab_thread, "get_venv_python", lambda: "python") + monkeypatch.setattr(jupyter_lab_thread, "default_interpreter", lambda: "python") monkeypatch.setattr(jupyter_lab_thread, "is_jupyter_installed", lambda _python: True) monkeypatch.setattr(jupyter_lab_thread.subprocess, "Popen", popen) monkeypatch.setattr( diff --git a/test/test_utils/test_jupyter_ready.py b/test/test_utils/test_jupyter_ready.py index e1dc663f..eea7e3ad 100644 --- a/test/test_utils/test_jupyter_ready.py +++ b/test/test_utils/test_jupyter_ready.py @@ -121,7 +121,7 @@ def terminate(self): self.terminated = True proc = _RecordingProc() - monkeypatch.setattr(mod, "get_venv_python", lambda: "python") + monkeypatch.setattr(mod, "default_interpreter", lambda: "python") monkeypatch.setattr(mod, "is_jupyter_installed", lambda exe: True) monkeypatch.setattr(mod, "find_free_port", lambda: 59999) monkeypatch.setattr(mod.subprocess, "Popen", lambda *a, **k: proc) @@ -199,24 +199,59 @@ class TestWhichInterpreterRunsTheLab: def test_the_one_chosen_in_the_ide_comes_first(self, monkeypatch): from pybreeze.pybreeze_ui.jupyter_lab_gui import jupyter_lab_thread as mod - monkeypatch.setattr(mod, "get_venv_python", lambda: "venv-python") + monkeypatch.setattr(mod, "default_interpreter", lambda: "run-python") assert mod.choose_python("C:/envs/project/python.exe") == "C:/envs/project/python.exe" - assert mod.choose_python(None) == "venv-python" + assert mod.choose_python(None) == "run-python" - def test_without_a_venv_the_ides_own_is_used(self, monkeypatch): - # It raised "Cannot find venv python executable" and the lab never started + def test_the_lab_runs_where_a_run_would(self, monkeypatch, tmp_path): + # An IDE started from a venv of its own ran the lab there, while a run of + # the same project used the project's .venv: its notebooks did not see + # the project's packages import sys + from pybreeze.extend.process_executor.python_task_process_manager import default_interpreter from pybreeze.pybreeze_ui.jupyter_lab_gui import jupyter_lab_thread as mod - def no_venv(): - raise RuntimeError("Cannot find venv python executable") + scripts = tmp_path / ".venv" / ("Scripts" if sys.platform == "win32" else "bin") + scripts.mkdir(parents=True) + project_python = scripts / ("python.exe" if sys.platform == "win32" else "python") + project_python.write_bytes(b"") + project_python.chmod(0o755) + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(sys, "base_prefix", sys.prefix + "-elsewhere") # the IDE runs in a venv - monkeypatch.setattr(mod, "get_venv_python", no_venv) + assert mod.choose_python(None) == default_interpreter() + assert mod.choose_python(None).startswith(str(scripts)) + + def test_without_a_venv_the_ides_own_is_used(self, monkeypatch, tmp_path): + import sys + + from pybreeze.pybreeze_ui.jupyter_lab_gui import jupyter_lab_thread as mod + + monkeypatch.chdir(tmp_path) assert mod.choose_python(None) == sys.executable + def test_a_packaged_build_with_no_python_says_so(self, qt_app, monkeypatch): + # default_interpreter raises JEditorExecException there, which the + # thread did not catch: it would have died with nothing shown + from je_editor import JEditorExecException + + from pybreeze.pybreeze_ui.jupyter_lab_gui import jupyter_lab_thread as mod + + def none_found(): + raise JEditorExecException("no python interpreter found") + + monkeypatch.setattr(mod, "default_interpreter", none_found) + thread = mod.JupyterLauncherThread() + errors: list = [] + thread.error_occurred.connect(errors.append) + + thread.run() + + assert errors == ["no python interpreter found"] + def test_installed_is_asked_of_the_interpreter_not_of_pip(self, tmp_path, monkeypatch): # A venv made without pip failed "pip show" with jupyterlab installed import os From 0cd6c3e1326e121c2ccccfa25555b38e09322fb7 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 04:59:19 +0800 Subject: [PATCH 114/166] Correct the Sphinx JupyterLab pages --- docs/source/Eng/jupyter_lab.rst | 19 ++++++++++++------- docs/source/Zh/jupyter_lab.rst | 14 ++++++++------ docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/source/Eng/jupyter_lab.rst b/docs/source/Eng/jupyter_lab.rst index dec27253..47d07b2b 100644 --- a/docs/source/Eng/jupyter_lab.rst +++ b/docs/source/Eng/jupyter_lab.rst @@ -7,21 +7,23 @@ Jupyter notebooks directly within the IDE. Opening JupyterLab ------------------- -JupyterLab is available from the tab menu. When opened, it creates a new tab -containing a full JupyterLab interface rendered via Qt's web engine. +Open it from **Tab > Tools Tab > JupyterLab**. It opens a new tab (titled +``JupyterLab ``) containing a full JupyterLab interface rendered via Qt's web engine. First-Time Setup ^^^^^^^^^^^^^^^^ -On first launch, if JupyterLab is not installed, PyBreeze will automatically -install it using pip. A status label shows the initialization progress. +If the interpreter the lab runs in cannot import ``jupyterlab``, PyBreeze first +installs it there with ``pip install -U jupyterlab``. A status label shows the progress. Interface --------- The JupyterLab tab contains: -- **Status Label** -- Shows initialization status ("Starting JupyterLab...", "Ready", etc.) +- **Status Label** -- Shows the startup status ("Initializing...", "Downloading..." while + JupyterLab is installed, "Loading... (Ns / 60s)" while the server starts) and is removed + once the lab loads; if the lab cannot start it reads "JupyterLab init failed: " - **Web Engine View** -- A full JupyterLab interface rendered in a ``QWebEngineView`` The embedded JupyterLab provides all standard Jupyter features: @@ -51,7 +53,10 @@ How It Works Usage Tips ---------- -- JupyterLab runs on a local port; no external network access is needed +- JupyterLab listens on a free port on localhost only; network access is needed only to + install JupyterLab when it is missing - You can open multiple notebooks in JupyterLab's own tab system - Use JupyterLab for data analysis, prototyping, and interactive testing -- The embedded JupyterLab shares the same Python environment as PyBreeze +- JupyterLab and its kernels run in the interpreter a script run uses: the one chosen under + **Python Env > Choose python interpreter**; with none chosen, a ``venv`` or ``.venv`` in + the working folder, else the interpreter PyBreeze itself runs on diff --git a/docs/source/Zh/jupyter_lab.rst b/docs/source/Zh/jupyter_lab.rst index 4a725d3d..6a54f063 100644 --- a/docs/source/Zh/jupyter_lab.rst +++ b/docs/source/Zh/jupyter_lab.rst @@ -6,21 +6,22 @@ PyBreeze 包含嵌入式 JupyterLab 環境,讓您可以直接在 IDE 中使用 開啟 JupyterLab ---------------- -JupyterLab 可從分頁選單中使用。開啟時,它會建立一個新分頁, +從 **Tab > Tools Tab > JupyterLab** 開啟。它會開一個新分頁(標題為 ``JupyterLab ``), 透過 Qt 的 Web 引擎呈現完整的 JupyterLab 介面。 首次設定 ^^^^^^^^ -首次啟動時,如果 JupyterLab 尚未安裝,PyBreeze 會自動使用 pip 安裝。 -狀態標籤會顯示初始化進度。 +若 lab 所用的直譯器無法匯入 ``jupyterlab``,PyBreeze 會先以 ``pip install -U jupyterlab`` +安裝到該直譯器。狀態標籤會顯示進度。 介面 ---- JupyterLab 分頁包含: -- **狀態標籤** -- 顯示初始化狀態(「Starting JupyterLab...」、「Ready」等) +- **狀態標籤** -- 顯示啟動狀態(「Initializing...」、安裝時的「Downloading...」、伺服器啟動時的 + 「Loading... (Ns / 60s)」),lab 載入後即移除;無法啟動時顯示「JupyterLab init failed: <原因>」 - **Web 引擎檢視** -- 在 ``QWebEngineView`` 中呈現的完整 JupyterLab 介面 嵌入式 JupyterLab 提供所有標準 Jupyter 功能: @@ -50,7 +51,8 @@ JupyterLab 分頁包含: 使用提示 -------- -- JupyterLab 在本地連接埠上執行;不需要外部網路存取 +- JupyterLab 只在 localhost 的空閒連接埠上執行;只有需要安裝 JupyterLab 時才需要網路 - 您可以在 JupyterLab 自己的分頁系統中開啟多個筆記本 - 使用 JupyterLab 進行資料分析、原型開發和互動式測試 -- 嵌入式 JupyterLab 與 PyBreeze 共用相同的 Python 環境 +- JupyterLab 與其 kernel 在執行腳本所用的直譯器中執行:**Python Env > Choose python interpreter** + 選定的直譯器;沒有選的話,用工作資料夾中的 ``venv`` 或 ``.venv``,再沒有就用 PyBreeze 自己的直譯器 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ed54036f..994187a3 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -541,3 +541,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Result / numbers**: 2589 tests in 143 files. `ruff check` clean. - **Files**: `pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py`, `test/test_utils/test_jupyter_ready.py`, `test/test_utils/test_jupyter_lifecycle.py`, `architecture_explore.md` - **Open items**: none. + +## U-20260925-63 · 2026-09-25 · The Sphinx JupyterLab pages told right · #docs #jupyter + +- **What**: a page-by-page audit of the Sphinx docs against the running IDE's menus and the code (the rename pass U-20260925-58 had fixed only labels) found the JupyterLab page, in both languages, saying: open it "from the tab menu" (it is Tab > Tools Tab > JupyterLab); it installs JupyterLab "on first launch" (whenever the lab's interpreter cannot import it); a status label reading "Starting JupyterLab...", "Ready" (neither string exists: "Initializing...", "Downloading...", "Loading... (Ns / 60s)", removed once the lab loads, or "JupyterLab init failed: "); "no external network access is needed" (the install needs it); and it "shares the same Python environment as PyBreeze" (it runs where a script run does, since U-20260925-62). +- **Change**: both pages say what the code does, with the tab's title `JupyterLab `. +- **Files**: `docs/source/Eng/jupyter_lab.rst`, `docs/source/Zh/jupyter_lab.rst` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 74a17f92..381159be 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-63 | 2026-09-25 | The Sphinx JupyterLab pages told right | #docs #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-62 | 2026-09-25 | JupyterLab runs in the interpreter a run uses | #fix #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-61 | 2026-09-25 | Bold and code in the Chinese Sphinx pages render, and the docs build without warnings | #fix #docs #i18n | [2026-09-c](2026-09-c.md) | | U-20260925-60 | 2026-09-25 | architecture.md's recipe for a new tool follows today's conventions | #docs | [2026-09-c](2026-09-c.md) | @@ -465,4 +466,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 62 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 63 | From 58240289c1f0d35a01e2a069350fad7c2461f47a Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 05:00:28 +0800 Subject: [PATCH 115/166] Correct the Sphinx SSH pages --- docs/source/Eng/ssh_client.rst | 61 +++++++++++++++++++++++----------- docs/source/Zh/ssh_client.rst | 48 ++++++++++++++++---------- docs/updates/2026-09-c.md | 7 ++++ docs/updates/README.md | 3 +- 4 files changed, 82 insertions(+), 37 deletions(-) diff --git a/docs/source/Eng/ssh_client.rst b/docs/source/Eng/ssh_client.rst index 2ae3df14..5cc5f075 100644 --- a/docs/source/Eng/ssh_client.rst +++ b/docs/source/Eng/ssh_client.rst @@ -2,12 +2,13 @@ SSH Client ========== PyBreeze includes a built-in SSH client for connecting to remote servers. -It can be opened from **Tools > SSH Client Tab** or **Tools > SSH Client Dock**. +It can be opened from **Tools > SSH > SSH Client Tab** or **Dock > SSH > SSH Client Dock**. Overview -------- -The SSH client provides three main components arranged in a horizontal splitter: +The SSH client shows the login widget at the top, above a horizontal splitter holding +the file tree and the command widget: 1. **Login Widget** (top) -- Connection settings 2. **File Tree** (left, ~30% width) -- Remote file browser @@ -25,15 +26,24 @@ The login widget provides fields for SSH connection: * - Field - Description * - **Host** - - The hostname or IP address of the remote server. + - The host name or IP address of the remote server. * - **Port** - The SSH port number (default: 22). - * - **Username** - - Your SSH username. - * - **Password / Key** - - Authentication credentials. Supports password-based and key-based authentication. - -After entering your credentials, click **Connect** to establish the SSH session. + * - **User** + - Your SSH user name. + * - **Use key auth** + - Tick for key-based authentication (choosing a key with **Browse...** ticks it). + * - **Key** / **Browse...** + - Path to the private key: an RSA, Ed25519 or ECDSA key in OpenSSH or PEM format, + PKCS#8 included (**Browse...** starts in ``~/.ssh``). A PuTTY ``.ppk`` key must first + be exported from PuTTYgen as an OpenSSH key; the error message says how. + * - **Password** + - The password; with key authentication it reads **Passphrase** and takes the key's + passphrase. + +After entering your credentials, click **Connect** (or press Enter in any field) to establish +the SSH session. An unknown host key is not accepted silently: its SHA256 fingerprint is shown +for confirmation on the first connection and kept in ``~/.pybreeze/ssh_known_hosts``. Remote File Browser ------------------- @@ -53,16 +63,24 @@ Right-click on any file or directory in the file tree to access: - Description * - **Refresh** - Reloads the current directory listing from the remote server. - * - **Create Folder** + * - **Create folder** - Creates a new directory on the remote server. * - **Rename** - - Renames the selected file or directory. + - Renames the selected file or directory (also **F2** while the tree has the focus). * - **Delete** - - Deletes the selected file or directory from the remote server. + - Deletes the selected file or directory from the remote server after a confirmation + (No is the default; also the **Delete** key while the tree has the focus). SFTP removes + only an empty folder. * - **Download** - Downloads the selected file to your local machine. - * - **Upload** - - Uploads a local file to the current remote directory. + * - **Upload to this folder** + - Uploads a local file into the folder right-clicked (or the folder holding the file + right-clicked), asking before it replaces a file on the server. + * - **Cancel the transfer** + - Shown while an upload or download runs; cancels it. + +Every request runs in the background, so a stalled link does not freeze the IDE, and both +directions write to a temporary file first, so a dropped link leaves the old copy whole. SSH Command Terminal -------------------- @@ -70,15 +88,20 @@ SSH Command Terminal The command widget provides an interactive terminal for executing commands on the remote server. -- Type commands and press Enter to execute -- Output is displayed in real-time -- Supports standard shell operations -- Command history is maintained during the session +- Type commands and press Enter to execute; Enter on an empty line reaches the shell too +- Output is displayed in real time, with ANSI colours, in a fixed-pitch font; the shell is + told the terminal's width and height as the view is resized +- **Up** and **Down** bring back earlier commands +- **Interrupt**, or Ctrl+C in the command line with nothing selected, stops what runs in the shell +- ``clear`` and ``reset`` wipe the view +- The view shows output line by line, so programs that draw on the whole screen by moving the + cursor, such as ``vim`` or ``htop``, come out garbled Usage Tips ---------- - Use the **Tab** mode to keep SSH alongside your code editor tabs - Use the **Dock** mode to position the SSH terminal on one side while coding -- The file browser supports drag-and-drop for uploads +- Upload and download from the file tree's right-click menu; F2 renames and Delete deletes + the entry in focus - You can have multiple SSH sessions open simultaneously in separate tabs/docks diff --git a/docs/source/Zh/ssh_client.rst b/docs/source/Zh/ssh_client.rst index dda50db1..0c421f18 100644 --- a/docs/source/Zh/ssh_client.rst +++ b/docs/source/Zh/ssh_client.rst @@ -2,12 +2,12 @@ SSH 用戶端 ========== PyBreeze 內建 SSH 用戶端,用於連接遠端伺服器。 -可以從 **Tools > SSH Client Tab** 或 **Tools > SSH Client Dock** 開啟。 +可以從 **Tools > SSH > SSH Client Tab** 或 **Dock > SSH > SSH Client Dock** 開啟。 總覽 ---- -SSH 用戶端提供三個主要元件,以水平分割器排列: +SSH 用戶端頂部是登入元件,下方的水平分割器放著檔案樹與命令元件: 1. **登入元件**\ (頂部)-- 連線設定 2. **檔案樹**\ (左側,約 30% 寬度)-- 遠端檔案瀏覽器 @@ -28,12 +28,19 @@ SSH 用戶端提供三個主要元件,以水平分割器排列: - 遠端伺服器的主機名稱或 IP 位址。 * - **Port** - SSH 連接埠號(預設:22)。 - * - **Username** + * - **User** - 您的 SSH 使用者名稱。 - * - **Password / Key** - - 驗證憑證。支援密碼驗證和金鑰驗證。 - -輸入憑證後,點擊 **Connect** 建立 SSH 連線。 + * - **Use key auth** + - 勾選以使用金鑰驗證(用 **Browse...** 選了金鑰也會勾選)。 + * - **Key** / **Browse...** + - 私鑰路徑:OpenSSH 或 PEM 格式的 RSA、Ed25519、ECDSA 金鑰,PKCS#8 也可以 + (**Browse...** 從 ``~/.ssh`` 開始)。PuTTY 的 ``.ppk`` 金鑰要先在 PuTTYgen 匯出成 + OpenSSH 金鑰,錯誤訊息會說明怎麼做。 + * - **Password** + - 密碼;使用金鑰驗證時改為 **Passphrase**,填金鑰的密語。 + +輸入憑證後,點擊 **Connect**\ (或在任一欄位按 Enter)建立 SSH 連線。未知的主機金鑰不會被默默接受: +第一次連線時會顯示它的 SHA256 指紋供確認,並保存在 ``~/.pybreeze/ssh_known_hosts``。 遠端檔案瀏覽器 -------------- @@ -53,31 +60,38 @@ SSH 用戶端提供三個主要元件,以水平分割器排列: - 說明 * - **Refresh** - 從遠端伺服器重新載入目前的目錄列表。 - * - **Create Folder** + * - **Create folder** - 在遠端伺服器上建立新目錄。 * - **Rename** - - 重新命名選定的檔案或目錄。 + - 重新命名選定的檔案或目錄(焦點在檔案樹時也可按 **F2**)。 * - **Delete** - - 從遠端伺服器刪除選定的檔案或目錄。 + - 確認後從遠端伺服器刪除選定的檔案或目錄(預設為「否」;焦點在檔案樹時也可按 + **Delete** 鍵)。SFTP 只能刪除空資料夾。 * - **Download** - 將選定的檔案下載到本機。 - * - **Upload** - - 將本機檔案上傳到目前的遠端目錄。 + * - **Upload to this folder** + - 將本機檔案上傳到按右鍵的資料夾(或按右鍵檔案所在的資料夾),要取代伺服器上的檔案前會先詢問。 + * - **Cancel the transfer** + - 上傳或下載進行中時出現,用來取消傳輸。 + +每個請求都在背景執行,連線卡住也不會讓 IDE 凍結;上下傳都會先寫入暫存檔,連線中斷時舊的檔案仍完整無缺。 SSH 命令終端機 -------------- 命令元件提供互動式終端機,用於在遠端伺服器上執行命令。 -- 輸入命令並按 Enter 執行 -- 即時顯示輸出 -- 支援標準 Shell 操作 -- 在連線期間維護命令歷史 +- 輸入命令並按 Enter 執行;空白的一行按 Enter 也會送到 shell +- 即時顯示輸出,顯示 ANSI 顏色、使用等寬字型;視窗大小改變時會把新的寬度與高度告訴 shell +- **上**、**下** 方向鍵叫回先前送出的命令 +- **Interrupt**,或在沒有選取文字的命令列按 Ctrl+C,可停止 shell 中正在執行的程式 +- ``clear`` 與 ``reset`` 會清空畫面 +- 畫面是一行一行顯示輸出,所以 ``vim``、``htop`` 這類移動游標畫滿整個畫面的程式會顯示錯亂 使用提示 -------- - 使用 **分頁** 模式將 SSH 與程式碼編輯器分頁並列 - 使用 **停靠面板** 模式將 SSH 終端機定位在一側,同時編輯程式碼 -- 檔案瀏覽器支援拖放上傳 +- 從檔案樹的右鍵選單上傳與下載;焦點在檔案樹時,F2 重新命名、Delete 刪除目前的項目 - 您可以在不同的分頁/停靠面板中同時開啟多個 SSH 連線 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 994187a3..b9ed35a5 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -548,3 +548,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: both pages say what the code does, with the tab's title `JupyterLab `. - **Files**: `docs/source/Eng/jupyter_lab.rst`, `docs/source/Zh/jupyter_lab.rst` - **Open items**: none. + +## U-20260925-64 · 2026-09-25 · The Sphinx SSH pages told right · #docs #ssh + +- **What**: the audit found the SSH Client page, in both languages, with a menu path that is not there (Tools > SSH Client Dock; it is Dock > SSH > SSH Client Dock, and the tab Tools > SSH > SSH Client Tab), three components "in a horizontal splitter" (the login widget sits above it), login fields named Username and "Password / Key" (User; Use key auth, Key with Browse..., and Password, which reads Passphrase under key authentication), menu entries named Create Folder and Upload (Create folder, Upload to this folder), no Cancel the transfer, and a file browser that "supports drag-and-drop for uploads" (nothing in the SSH code handles a drag or a drop). +- **Change**: both pages give the real paths, layout, fields and entries, and what the README says of the client (the key formats and the PuTTY note, trust on first use, F2 and Delete, Delete asking with No as the default and SFTP removing only an empty folder, background requests and temporary files, colours, the pty size, history, Interrupt, `clear`, and the line-by-line view). +- **Files**: `docs/source/Eng/ssh_client.rst`, `docs/source/Zh/ssh_client.rst` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 381159be..f0cdad02 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-64 | 2026-09-25 | The Sphinx SSH pages told right | #docs #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-63 | 2026-09-25 | The Sphinx JupyterLab pages told right | #docs #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-62 | 2026-09-25 | JupyterLab runs in the interpreter a run uses | #fix #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-61 | 2026-09-25 | Bold and code in the Chinese Sphinx pages render, and the docs build without warnings | #fix #docs #i18n | [2026-09-c](2026-09-c.md) | @@ -466,4 +467,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 63 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 64 | From 85b890fba2d6917441a0abbebc75399022612f35 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 05:01:16 +0800 Subject: [PATCH 116/166] Correct the Sphinx Install pages --- docs/source/Eng/menu_install.rst | 5 ++++- docs/source/Zh/menu_install.rst | 5 ++++- docs/updates/2026-09-c.md | 7 +++++++ docs/updates/README.md | 3 ++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/source/Eng/menu_install.rst b/docs/source/Eng/menu_install.rst index 9d206aca..e50489d0 100644 --- a/docs/source/Eng/menu_install.rst +++ b/docs/source/Eng/menu_install.rst @@ -27,6 +27,8 @@ Installs the automation module packages via pip. - Runs ``pip install -U automation_file`` * - **Install MailThunder** - Runs ``pip install -U je_mail_thunder`` + * - **Install TestPioneer** + - Runs ``pip install -U test_pioneer`` * - **Install prthinker (code review)** - prthinker is not on PyPI. The first time, asks for its source folder and remembers it; then runs ``pip install -U [runner]`` @@ -50,4 +52,5 @@ Tools Submenu with no shell in between, so a folder name holding ``&`` or ``|`` reaches pip unchanged. pip runs with the interpreter chosen in the Python environment menu; when none is chosen, with a ``venv`` or ``.venv`` in the working - directory, and otherwise with the Python found on ``PATH``. + directory, and otherwise with the interpreter PyBreeze itself runs on (only a + packaged build looks on ``PATH``). diff --git a/docs/source/Zh/menu_install.rst b/docs/source/Zh/menu_install.rst index 21956d0a..b530be30 100644 --- a/docs/source/Zh/menu_install.rst +++ b/docs/source/Zh/menu_install.rst @@ -27,6 +27,8 @@ Automation 子選單 - 執行 ``pip install -U automation_file`` * - **Install MailThunder** - 執行 ``pip install -U je_mail_thunder`` + * - **Install TestPioneer** + - 執行 ``pip install -U test_pioneer`` * - **Install prthinker (code review)** - prthinker 不在 PyPI 上。第一次會詢問它的原始碼資料夾並記下來, 之後執行 ``pip install -U <資料夾>[runner]`` @@ -48,4 +50,5 @@ Tools 子選單 每次安裝都會開一個自己的執行視窗,在裡面執行 ``python -m pip``,中間不經過 shell, 所以資料夾名稱裡有 ``&`` 或 ``|`` 也會原樣交給 pip。pip 使用 Python 環境選單選定的 - 直譯器;沒有選的話,使用工作目錄下的 ``venv`` 或 ``.venv``,再沒有就用 ``PATH`` 上找到的 Python。 + 直譯器;沒有選的話,使用工作目錄下的 ``venv`` 或 ``.venv``,再沒有就用 PyBreeze 自己執行所用的直譯器 + (只有打包版才會去 ``PATH`` 上找)。 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index b9ed35a5..bf2b8e48 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -555,3 +555,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: both pages give the real paths, layout, fields and entries, and what the README says of the client (the key formats and the PuTTY note, trust on first use, F2 and Delete, Delete asking with No as the default and SFTP removing only an empty folder, background requests and temporary files, colours, the pty size, history, Interrupt, `clear`, and the line-by-line view). - **Files**: `docs/source/Eng/ssh_client.rst`, `docs/source/Zh/ssh_client.rst` - **Open items**: none. + +## U-20260925-65 · 2026-09-25 · The Sphinx Install pages list TestPioneer and the right fallback interpreter · #docs + +- **What**: the audit found the Install Menu page, in both languages, without the Install TestPioneer entry (U-20260924-302), and saying pip runs, with no interpreter chosen and no venv, with the Python on `PATH`; it runs with the IDE's own interpreter, and only a packaged build looks on `PATH` (`default_interpreter`, as U-20260925-54 corrected in the README). +- **Change**: both pages list Install TestPioneer (`pip install -U test_pioneer`) after MailThunder, as the menu does, and give the right fallback. +- **Files**: `docs/source/Eng/menu_install.rst`, `docs/source/Zh/menu_install.rst` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f0cdad02..8a61055d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-65 | 2026-09-25 | The Sphinx Install pages list TestPioneer and the right fallback interpreter | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-64 | 2026-09-25 | The Sphinx SSH pages told right | #docs #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-63 | 2026-09-25 | The Sphinx JupyterLab pages told right | #docs #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-62 | 2026-09-25 | JupyterLab runs in the interpreter a run uses | #fix #jupyter | [2026-09-c](2026-09-c.md) | @@ -467,4 +468,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 64 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 65 | From 0d908f7bab980b0dcfded6aa2663fabc67a7def1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 05:03:24 +0800 Subject: [PATCH 117/166] Correct the Sphinx Automation pages --- docs/source/Eng/menu_automation.rst | 109 ++++++++++++++++++---------- docs/source/Zh/menu_automation.rst | 104 ++++++++++++++++---------- docs/updates/2026-09-c.md | 7 ++ docs/updates/README.md | 3 +- 4 files changed, 146 insertions(+), 77 deletions(-) diff --git a/docs/source/Eng/menu_automation.rst b/docs/source/Eng/menu_automation.rst index 566e2628..a6030f91 100644 --- a/docs/source/Eng/menu_automation.rst +++ b/docs/source/Eng/menu_automation.rst @@ -7,10 +7,12 @@ and email automation. Each automation module follows a consistent menu structure: -- **RUN** submenu -- Execute scripts (single or multi-file, with or without email reporting) -- **Help** submenu -- Links to documentation and GitHub repository -- **Project** submenu -- Create new project templates -- **GUI Tab** -- Open an embedded GUI widget (available for some modules) +- **Run** submenu -- Execute scripts (single or multi-file, with or without email reporting) +- **Help** submenu -- Links to documentation and GitHub repository, opened as browser tabs inside the IDE +- **Project** submenu -- Create a new project template in the IDE's working directory +- ** GUI** -- Open the module's GUI as a tab (**APITestka GUI**, **AutoControl GUI**, **LoadDensity GUI**) + +**TestPioneer** and **Code Review (prthinker)** have menus of their own, described below. AutoControl Menu ---------------- @@ -18,7 +20,7 @@ AutoControl Menu **AutoControl** is a GUI automation module for desktop application testing. It can record and replay mouse/keyboard actions. -RUN Submenu +Run Submenu ^^^^^^^^^^^ .. list-table:: @@ -29,7 +31,7 @@ RUN Submenu - Description * - **Run AutoControl Script** - Executes the current editor content as an AutoControl script. - * - **Run AutoControl Script With Send** + * - **Run AutoControl With Send** - Executes the script and sends the results via email (using MailThunder). * - **Run Multi AutoControl Script** - Runs multiple AutoControl scripts from a selected directory. @@ -59,8 +61,8 @@ Project Submenu * - Menu Item - Description - * - **Create Project** - - Creates a new AutoControl project structure using ``je_auto_control``. + * - **Create AutoControl Project** + - Creates a AutoControl project template (``je_auto_control``) in the IDE's working directory, asking before it replaces one that is already there. Record Submenu ^^^^^^^^^^^^^^ @@ -74,13 +76,15 @@ mouse and keyboard actions for playback. * - Menu Item - Description - * - **Start Record** + * - **Record Start** - Starts recording mouse and keyboard actions. - * - **Stop Record** - - Stops recording and inserts the recorded action data into the code editor. + * - **Record Stop** + - Stops recording and inserts the recorded actions, as the JSON the AutoControl runner + reads, at the cursor of the editor tab in front; with no editor tab in front they are + copied to the clipboard. If nothing was recorded, it says so. -GUI Tab -^^^^^^^ +AutoControl GUI +^^^^^^^^^^^^^^^ Opens an embedded AutoControl GUI widget as a new tab in the editor, providing a visual interface for AutoControl operations. @@ -91,7 +95,7 @@ APITestka Menu **APITestka** is an API testing automation module for sending HTTP requests and validating responses. -RUN Submenu +Run Submenu ^^^^^^^^^^^ .. list-table:: @@ -102,7 +106,7 @@ RUN Submenu - Description * - **Run APITestka Script** - Executes the current editor content as an APITestka script. - * - **Run APITestka Script With Send** + * - **Run APITestka With Send** - Executes the script and sends results via email. * - **Run Multi APITestka Script** - Runs multiple APITestka scripts from a selected directory. @@ -118,10 +122,10 @@ Help Submenu Project Submenu ^^^^^^^^^^^^^^^ -- **Create Project** -- Creates a new APITestka project structure using ``je_api_testka``. +- **Create APITestka Project** -- Creates a APITestka project template (``je_api_testka``) in the IDE's working directory, asking before it replaces one that is already there. -GUI Tab -^^^^^^^ +APITestka GUI +^^^^^^^^^^^^^ Opens an embedded APITestka GUI widget as a new tab for visual API testing. @@ -131,7 +135,7 @@ WebRunner Menu **WebRunner** is a web browser automation module for testing web applications using browser drivers (Selenium-based). -RUN Submenu +Run Submenu ^^^^^^^^^^^ .. list-table:: @@ -142,7 +146,7 @@ RUN Submenu - Description * - **Run WebRunner Script** - Executes the current editor content as a WebRunner script. - * - **Run WebRunner Script With Send** + * - **Run WebRunner With Send** - Executes the script and sends results via email. * - **Run Multi WebRunner Script** - Runs multiple WebRunner scripts from a selected directory. @@ -158,7 +162,7 @@ Help Submenu Project Submenu ^^^^^^^^^^^^^^^ -- **Create Project** -- Creates a new WebRunner project structure using ``je_web_runner``. +- **Create WebRunner Project** -- Creates a WebRunner project template (``je_web_runner``) in the IDE's working directory, asking before it replaces one that is already there. LoadDensity Menu ---------------- @@ -166,7 +170,7 @@ LoadDensity Menu **LoadDensity** is a load/performance testing module that generates concurrent requests to test system capacity. -RUN Submenu +Run Submenu ^^^^^^^^^^^ .. list-table:: @@ -177,7 +181,7 @@ RUN Submenu - Description * - **Run LoadDensity Script** - Executes the current editor content as a LoadDensity script. - * - **Run LoadDensity Script With Send** + * - **Run LoadDensity With Send** - Executes the script and sends results via email. * - **Run Multi LoadDensity Script** - Runs multiple LoadDensity scripts from a selected directory. @@ -193,10 +197,10 @@ Help Submenu Project Submenu ^^^^^^^^^^^^^^^ -- **Create Project** -- Creates a new LoadDensity project structure using ``je_load_density``. +- **Create LoadDensity Project** -- Creates a LoadDensity project template (``je_load_density``) in the IDE's working directory, asking before it replaces one that is already there. -GUI Tab -^^^^^^^ +LoadDensity GUI +^^^^^^^^^^^^^^^ Opens an embedded LoadDensity GUI widget as a new tab for visual load test configuration. @@ -206,7 +210,7 @@ FileAutomation Menu **FileAutomation** is a file operation automation module for automating file system tasks such as copying, moving, renaming, and processing files. -RUN Submenu +Run Submenu ^^^^^^^^^^^ .. list-table:: @@ -217,7 +221,7 @@ RUN Submenu - Description * - **Run FileAutomation Script** - Executes the current editor content as a FileAutomation script. - * - **Run FileAutomation Script With Send** + * - **Run FileAutomation With Send** - Executes the script and sends results via email. * - **Run Multi FileAutomation Script** - Runs multiple FileAutomation scripts from a selected directory. @@ -233,7 +237,7 @@ Help Submenu Project Submenu ^^^^^^^^^^^^^^^ -- **Create Project** -- Creates a new FileAutomation project structure using ``automation_file``. +- **Create FileAutomation Project** -- Creates a FileAutomation project template (``automation_file``) in the IDE's working directory, asking before it replaces one that is already there. MailThunder Menu ---------------- @@ -241,7 +245,7 @@ MailThunder Menu **MailThunder** is an email automation module for sending test reports and automated notifications. -RUN Submenu +Run Submenu ^^^^^^^^^^^ .. list-table:: @@ -262,7 +266,7 @@ Help Submenu Project Submenu ^^^^^^^^^^^^^^^ -- **Create Project** -- Creates a new MailThunder project structure. +- **Create MailThunder Project** -- Creates a MailThunder project template (``je_mail_thunder``) in the IDE's working directory, asking before it replaces one that is already there. TestPioneer Menu ---------------- @@ -276,10 +280,38 @@ TestPioneer Menu * - Menu Item - Description * - **Create TestPioneer YAML Template** - - Generates a YAML template file for defining test cases. + - Creates ``.TestPioneer/.TestPioneer.yml`` in the IDE's working directory, asking + before it replaces one that is already there. * - **Run TestPioneer YAML** - - Opens a file dialog to select a ``.yml`` file and executes the test + - Opens a file dialog to select a ``.yml`` or ``.yaml`` file and executes the test definitions within it. + * - **Help > Open TestPioneer GitHub** + - Opens the TestPioneer GitHub repository (its README is its manual) in a browser tab. + +Code Review (prthinker) Menu +---------------------------- + +Runs the prthinker chain-of-thought code review; output streams into a run window. +Install prthinker first with **Install > Automation > Install prthinker (code review)**. + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Menu Item + - Description + * - **Review the current file** + - Saves the file in the editor tab in front, then reviews it. + * - **Review a Pull Request** + - Asks for the pull request number and reviews that pull request of the repository + set in **Settings**. + * - **Settings** + - Opens the prthinker settings: inference backend, model, code host, repository, + keys and tokens. + * - **Help > Open prthinker documentation** + - Opens https://code-review-framework.readthedocs.io/ in a browser tab. + * - **Help > Open prthinker GitHub** + - Opens the prthinker GitHub repository in a browser tab. Script Execution Flow --------------------- @@ -289,7 +321,8 @@ When you run any automation script, the following process occurs: 1. The current code editor content is captured. 2. A subprocess is spawned using the ``TaskProcessManager``. 3. The script runs in an isolated process (preventing crashes from affecting the IDE). -4. A **Code Output Window** opens to display real-time execution output. +4. A run window opens to display real-time execution output, titled with the package + and the file it runs, with a **Stop** button. 5. If "With Send" was selected, results are sent via MailThunder email after execution. .. note:: @@ -302,10 +335,10 @@ Multi-Script Execution When using the "Run Multi" options: -1. A directory selection dialog opens. -2. All matching script files in the selected directory are collected. -3. Each script is executed sequentially in its own subprocess. -4. Results from all scripts are aggregated. +1. A folder selection dialog opens. +2. Every ``.json`` action file in the folder and its subfolders is collected; a folder with none says so. +3. The files run one after another, each in its own subprocess and its own run window. +4. Each run reports on its own (and, with "With Send", mails its own report); stopping one run ends the batch. Report Formats ^^^^^^^^^^^^^^ diff --git a/docs/source/Zh/menu_automation.rst b/docs/source/Zh/menu_automation.rst index 59b8ff9d..9832e1be 100644 --- a/docs/source/Zh/menu_automation.rst +++ b/docs/source/Zh/menu_automation.rst @@ -6,10 +6,12 @@ Automation 選單 每個自動化模組都遵循一致的選單結構: -- **RUN** 子選單 -- 執行腳本(單檔或多檔,可選擇是否以郵件發送結果) -- **Help** 子選單 -- 連結到文件和 GitHub 儲存庫 -- **Project** 子選單 -- 建立新的專案範本 -- **GUI Tab** -- 開啟嵌入式 GUI 元件(部分模組可用) +- **Run** 子選單 -- 執行腳本(單檔或多檔,可選擇是否以郵件發送結果) +- **Help** 子選單 -- 在 IDE 內的瀏覽器分頁開啟文件和 GitHub 儲存庫 +- **Project** 子選單 -- 在 IDE 的工作目錄建立新的專案範本 +- **<模組> GUI** -- 以分頁開啟該模組的 GUI(**APITestka GUI**、**AutoControl GUI**、**LoadDensity GUI**) + +**TestPioneer** 與 **Code Review (prthinker)** 的選單另有結構,見下文。 AutoControl 選單 ----------------- @@ -17,7 +19,7 @@ AutoControl 選單 **AutoControl** 是桌面應用程式測試的 GUI 自動化模組, 可以錄製和重播滑鼠/鍵盤操作。 -RUN 子選單 +Run 子選單 ^^^^^^^^^^ .. list-table:: @@ -28,7 +30,7 @@ RUN 子選單 - 說明 * - **Run AutoControl Script** - 將目前編輯器內容作為 AutoControl 腳本執行。 - * - **Run AutoControl Script With Send** + * - **Run AutoControl With Send** - 執行腳本並透過郵件發送結果(使用 MailThunder)。 * - **Run Multi AutoControl Script** - 從選定的目錄執行多個 AutoControl 腳本。 @@ -58,8 +60,8 @@ Project 子選單 * - 選單項目 - 說明 - * - **Create Project** - - 使用 ``je_auto_control`` 建立新的 AutoControl 專案結構。 + * - **Create AutoControl Project** + - 在 IDE 的工作目錄建立 AutoControl 專案範本(``je_auto_control``),已存在時會先詢問是否取代。 Record 子選單 ^^^^^^^^^^^^^ @@ -72,13 +74,14 @@ Record 子選單是 **AutoControl 獨有** 的功能,允許您錄製滑鼠和 * - 選單項目 - 說明 - * - **Start Record** + * - **Record Start** - 開始錄製滑鼠和鍵盤操作。 - * - **Stop Record** - - 停止錄製並將錄製的操作資料插入到程式碼編輯器中。 + * - **Record Stop** + - 停止錄製,並把錄到的操作以 AutoControl 執行器讀得懂的 JSON 插入到前景編輯分頁的游標處; + 前景不是編輯分頁時改為複製到剪貼簿。沒有錄到任何東西時會提示。 -GUI Tab -^^^^^^^ +AutoControl GUI +^^^^^^^^^^^^^^^ 在編輯器中開啟嵌入式 AutoControl GUI 元件作為新分頁, 提供 AutoControl 操作的視覺化介面。 @@ -88,7 +91,7 @@ APITestka 選單 **APITestka** 是 API 測試自動化模組,用於發送 HTTP 請求和驗證回應。 -RUN 子選單 +Run 子選單 ^^^^^^^^^^ .. list-table:: @@ -99,7 +102,7 @@ RUN 子選單 - 說明 * - **Run APITestka Script** - 將目前編輯器內容作為 APITestka 腳本執行。 - * - **Run APITestka Script With Send** + * - **Run APITestka With Send** - 執行腳本並透過郵件發送結果。 * - **Run Multi APITestka Script** - 從選定的目錄執行多個 APITestka 腳本。 @@ -115,10 +118,10 @@ Help 子選單 Project 子選單 ^^^^^^^^^^^^^^ -- **Create Project** -- 使用 ``je_api_testka`` 建立新的 APITestka 專案結構。 +- **Create APITestka Project** -- 在 IDE 的工作目錄建立 APITestka 專案範本(``je_api_testka``),已存在時會先詢問是否取代。 -GUI Tab -^^^^^^^ +APITestka GUI +^^^^^^^^^^^^^ 在編輯器中開啟嵌入式 APITestka GUI 元件作為新分頁,用於視覺化 API 測試。 @@ -128,7 +131,7 @@ WebRunner 選單 **WebRunner** 是網頁瀏覽器自動化模組,使用瀏覽器驅動程式(基於 Selenium) 測試網頁應用程式。 -RUN 子選單 +Run 子選單 ^^^^^^^^^^ .. list-table:: @@ -139,7 +142,7 @@ RUN 子選單 - 說明 * - **Run WebRunner Script** - 將目前編輯器內容作為 WebRunner 腳本執行。 - * - **Run WebRunner Script With Send** + * - **Run WebRunner With Send** - 執行腳本並透過郵件發送結果。 * - **Run Multi WebRunner Script** - 從選定的目錄執行多個 WebRunner 腳本。 @@ -155,14 +158,14 @@ Help 子選單 Project 子選單 ^^^^^^^^^^^^^^ -- **Create Project** -- 使用 ``je_web_runner`` 建立新的 WebRunner 專案結構。 +- **Create WebRunner Project** -- 在 IDE 的工作目錄建立 WebRunner 專案範本(``je_web_runner``),已存在時會先詢問是否取代。 LoadDensity 選單 ---------------- **LoadDensity** 是負載/效能測試模組,產生並行請求以測試系統容量。 -RUN 子選單 +Run 子選單 ^^^^^^^^^^ .. list-table:: @@ -173,7 +176,7 @@ RUN 子選單 - 說明 * - **Run LoadDensity Script** - 將目前編輯器內容作為 LoadDensity 腳本執行。 - * - **Run LoadDensity Script With Send** + * - **Run LoadDensity With Send** - 執行腳本並透過郵件發送結果。 * - **Run Multi LoadDensity Script** - 從選定的目錄執行多個 LoadDensity 腳本。 @@ -189,10 +192,10 @@ Help 子選單 Project 子選單 ^^^^^^^^^^^^^^ -- **Create Project** -- 使用 ``je_load_density`` 建立新的 LoadDensity 專案結構。 +- **Create LoadDensity Project** -- 在 IDE 的工作目錄建立 LoadDensity 專案範本(``je_load_density``),已存在時會先詢問是否取代。 -GUI Tab -^^^^^^^ +LoadDensity GUI +^^^^^^^^^^^^^^^ 在編輯器中開啟嵌入式 LoadDensity GUI 元件作為新分頁,用於視覺化負載測試設定。 @@ -202,7 +205,7 @@ FileAutomation 選單 **FileAutomation** 是檔案操作自動化模組,用於自動化檔案系統任務, 如複製、移動、重新命名和處理檔案。 -RUN 子選單 +Run 子選單 ^^^^^^^^^^ .. list-table:: @@ -213,7 +216,7 @@ RUN 子選單 - 說明 * - **Run FileAutomation Script** - 將目前編輯器內容作為 FileAutomation 腳本執行。 - * - **Run FileAutomation Script With Send** + * - **Run FileAutomation With Send** - 執行腳本並透過郵件發送結果。 * - **Run Multi FileAutomation Script** - 從選定的目錄執行多個 FileAutomation 腳本。 @@ -229,14 +232,14 @@ Help 子選單 Project 子選單 ^^^^^^^^^^^^^^ -- **Create Project** -- 使用 ``automation_file`` 建立新的 FileAutomation 專案結構。 +- **Create FileAutomation Project** -- 在 IDE 的工作目錄建立 FileAutomation 專案範本(``automation_file``),已存在時會先詢問是否取代。 MailThunder 選單 ----------------- **MailThunder** 是郵件自動化模組,用於發送測試報告和自動化通知。 -RUN 子選單 +Run 子選單 ^^^^^^^^^^ .. list-table:: @@ -257,7 +260,7 @@ Help 子選單 Project 子選單 ^^^^^^^^^^^^^^ -- **Create Project** -- 建立新的 MailThunder 專案結構。 +- **Create MailThunder Project** -- 在 IDE 的工作目錄建立 MailThunder 專案範本(``je_mail_thunder``),已存在時會先詢問是否取代。 TestPioneer 選單 ----------------- @@ -271,9 +274,34 @@ TestPioneer 選單 * - 選單項目 - 說明 * - **Create TestPioneer YAML Template** - - 產生用於定義測試案例的 YAML 範本檔案。 + - 在 IDE 的工作目錄建立 ``.TestPioneer/.TestPioneer.yml``,已存在時會先詢問是否取代。 * - **Run TestPioneer YAML** - - 開啟檔案對話框選擇 ``.yml`` 檔案,並執行其中定義的測試。 + - 開啟檔案對話框選擇 ``.yml`` 或 ``.yaml`` 檔案,並執行其中定義的測試。 + * - **Help > Open TestPioneer GitHub** + - 在瀏覽器分頁開啟 TestPioneer GitHub 儲存庫(它的 README 就是手冊)。 + +Code Review (prthinker) 選單 +---------------------------- + +執行 prthinker 思維鏈程式碼審查,輸出串流到執行視窗。請先用 +**Install > Automation > Install prthinker (code review)** 安裝 prthinker。 + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - 選單項目 + - 說明 + * - **Review the current file** + - 先儲存前景編輯分頁的檔案,再審查它。 + * - **Review a Pull Request** + - 詢問 Pull Request 編號,審查 **Settings** 中設定之儲存庫的該 Pull Request。 + * - **Settings** + - 開啟 prthinker 設定:推論後端、模型、程式碼託管平台、儲存庫、金鑰與權杖。 + * - **Help > Open prthinker documentation** + - 在瀏覽器分頁開啟 https://code-review-framework.readthedocs.io/ 。 + * - **Help > Open prthinker GitHub** + - 在瀏覽器分頁開啟 prthinker GitHub 儲存庫。 腳本執行流程 ------------ @@ -283,7 +311,7 @@ TestPioneer 選單 1. 擷取目前程式碼編輯器的內容。 2. 使用 ``TaskProcessManager`` 產生子程序。 3. 腳本在獨立的程序中執行(防止崩潰影響 IDE)。 -4. 開啟 **程式碼輸出視窗** 即時顯示執行輸出。 +4. 開啟執行視窗即時顯示執行輸出,標題是套件名稱與它執行的檔案,並有 **Stop** 按鈕。 5. 如果選擇了「With Send」,執行完成後透過 MailThunder 郵件發送結果。 .. note:: @@ -296,10 +324,10 @@ TestPioneer 選單 使用「Run Multi」選項時: -1. 開啟目錄選擇對話框。 -2. 收集選定目錄中所有符合的腳本檔案。 -3. 每個腳本在各自的子程序中依序執行。 -4. 彙整所有腳本的結果。 +1. 開啟資料夾選擇對話框。 +2. 收集該資料夾及其子資料夾中所有 ``.json`` 動作檔;沒有時會提示。 +3. 檔案依序執行,每個都在自己的子程序與自己的執行視窗中。 +4. 每次執行各自回報結果(選「With Send」時各自寄出報告);停止其中一次執行會結束整批。 報告格式 ^^^^^^^^ diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index bf2b8e48..96f48638 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -562,3 +562,10 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: both pages list Install TestPioneer (`pip install -U test_pioneer`) after MailThunder, as the menu does, and give the right fallback. - **Files**: `docs/source/Eng/menu_install.rst`, `docs/source/Zh/menu_install.rst` - **Open items**: none. + +## U-20260925-66 · 2026-09-25 · The Sphinx Automation pages told right · #docs #menu + +- **What**: the audit found the Automation Menu page, in both languages, naming entries that are not there: a "RUN" submenu (Run), "Run X Script With Send" (Run X With Send, for AutoControl, APITestka, WebRunner, LoadDensity and FileAutomation), a "Create Project" entry (Create X Project), "Start Record" / "Stop Record" (Record Start / Record Stop) and a "GUI Tab" (APITestka GUI, AutoControl GUI, LoadDensity GUI). It left out TestPioneer's Help submenu, where its template goes, that its file dialog takes `.yaml` too, and the whole Code Review (prthinker) menu. It said a multi-script run gathers "all matching script files" and "aggregates" their results; it collects every `.json` action file in the folder and its subfolders, runs them one after another each in a run window of its own, each reporting (and with Send, mailing) on its own, and stopping one ends the batch. +- **Change**: both pages use the menu's labels, say where a project or template is created and that it asks before replacing one, say what Record Stop does with no editor tab in front or nothing recorded, add TestPioneer's Help and the prthinker menu (its four entries and help links, and that prthinker is installed from the Install menu), describe the multi-script run as it is, and the run window's title and Stop button. +- **Files**: `docs/source/Eng/menu_automation.rst`, `docs/source/Zh/menu_automation.rst` +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8a61055d..a532dfe0 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-66 | 2026-09-25 | The Sphinx Automation pages told right | #docs #menu | [2026-09-c](2026-09-c.md) | | U-20260925-65 | 2026-09-25 | The Sphinx Install pages list TestPioneer and the right fallback interpreter | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-64 | 2026-09-25 | The Sphinx SSH pages told right | #docs #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-63 | 2026-09-25 | The Sphinx JupyterLab pages told right | #docs #jupyter | [2026-09-c](2026-09-c.md) | @@ -468,4 +469,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 65 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 66 | From 1cc9c3a423156d80ae424a110b545479e2083a94 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 05:07:31 +0800 Subject: [PATCH 118/166] Ask about the diagram's unsaved edits from one method --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- .../pybreeze_ui/diagram_editor/diagram_editor_widget.py | 9 ++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 96f48638..b34c8ba0 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -569,3 +569,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Change**: both pages use the menu's labels, say where a project or template is created and that it asks before replacing one, say what Record Stop does with no editor tab in front or nothing recorded, add TestPioneer's Help and the prthinker menu (its four entries and help links, and that prthinker is installed from the Install menu), describe the multi-script run as it is, and the run window's title and Stop button. - **Files**: `docs/source/Eng/menu_automation.rst`, `docs/source/Zh/menu_automation.rst` - **Open items**: none. + +## U-20260925-67 · 2026-09-25 · The diagram editor's unsaved-edits question in one place · #refactor #diagram + +- **What**: refactor, no change in behaviour. The question the diagram editor asks before unsaved changes are lost lived inside `may_close`; opening another diagram needs it too (the next entry). +- **Change**: `DiagramEditorWidget._may_discard_edits(question_key, fallback)` asks it, as the prompt editor's method of the same name does; `may_close` calls it with `diagram_editor_close_over_edits`. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py` +- **Checks**: `test_unsaved_on_close.py` and `test_diagram_editor_widget.py` pass unchanged (27); `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a532dfe0..afa87e5c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-67 | 2026-09-25 | The diagram editor's unsaved-edits question in one place | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-66 | 2026-09-25 | The Sphinx Automation pages told right | #docs #menu | [2026-09-c](2026-09-c.md) | | U-20260925-65 | 2026-09-25 | The Sphinx Install pages list TestPioneer and the right fallback interpreter | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-64 | 2026-09-25 | The Sphinx SSH pages told right | #docs #ssh | [2026-09-c](2026-09-c.md) | @@ -469,4 +470,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 66 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 67 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py index 6b7ca386..2436f0ba 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py @@ -496,12 +496,15 @@ def may_close(self) -> bool: Asked by the main window before it closes this tab or the IDE: an unsaved diagram was lost on close without a word. """ + return self._may_discard_edits( + "diagram_editor_close_over_edits", "The diagram has changes that are not saved. Close and lose them?") + + def _may_discard_edits(self, question_key: str, fallback: str) -> bool: + """True when the diagram is as last saved or opened, or the user answers Yes to *question_key*.""" if self._scene.undo_stack.isClean(): return True reply = QMessageBox.question( - self, _lang("unsaved_close_title", "Unsaved changes"), - _lang("diagram_editor_close_over_edits", - "The diagram has changes that are not saved. Close and lose them?"), + self, _lang("unsaved_close_title", "Unsaved changes"), _lang(question_key, fallback), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) return reply == QMessageBox.StandardButton.Yes From 5133f0ae5eac6c567372558f6774a7963b47d780 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 05:09:07 +0800 Subject: [PATCH 119/166] Ask before opening a diagram over unsaved changes --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 +- docs/source/Eng/menu_tools.rst | 3 +- docs/source/Zh/menu_tools.rst | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- .../extend_multi_language/extend_english.py | 2 + .../extend_traditional_chinese.py | 2 + .../diagram_editor/diagram_editor_widget.py | 5 ++ test/test_utils/test_unsaved_on_close.py | 55 ++++++++++++++++++- 12 files changed, 81 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f8f45d66..a37fb720 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 758 keys, and a test enforces that parity so a new string can never land in one language only. The Language menu also lists JEditor's Japanese and Simplified Chinese: picked, JEditor's own menus change and PyBreeze's strings stay in English. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 759 keys, and a test enforces that parity so a new string can never land in one language only. The Language menu also lists JEditor's Japanese and Simplified Chinese: picked, JEditor's own menus change and PyBreeze's strings stay in English. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index e75725fb..12e2bb62 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -215,7 +215,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 758 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。语言菜单还列出 JEditor 的日文与简体中文:选择后 JEditor 自己的菜单会随之改变,PyBreeze 的字符串则保持英文。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 759 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。语言菜单还列出 JEditor 的日文与简体中文:选择后 JEditor 自己的菜单会随之改变,PyBreeze 的字符串则保持英文。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index ccf79ba9..aec6eb6a 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -215,7 +215,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 758 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。語言選單另外列出 JEditor 的日文與簡體中文:選了之後 JEditor 自己的選單會改變,PyBreeze 的字串則維持英文。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 759 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。語言選單另外列出 JEditor 的日文與簡體中文:選了之後 JEditor 自己的選單會改變,PyBreeze 的字串則維持英文。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index e53fcf63..b07b0e4a 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -369,7 +369,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 758 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 759 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2589 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2592 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/source/Eng/menu_tools.rst b/docs/source/Eng/menu_tools.rst index bea7922d..90c4f6f7 100644 --- a/docs/source/Eng/menu_tools.rst +++ b/docs/source/Eng/menu_tools.rst @@ -80,7 +80,8 @@ File operations * - **New** - Discards the current diagram (with confirmation if non-empty). * - **Open** - - Loads a previously saved ``.diagram.json`` file. + - Loads a previously saved ``.diagram.json`` file, asking first when the diagram + has changes that are not saved. * - **Save** / **Save As** (``Ctrl+S`` / ``Ctrl+Shift+S``) - Saves the diagram as ``.diagram.json``. * - **Import Mermaid** diff --git a/docs/source/Zh/menu_tools.rst b/docs/source/Zh/menu_tools.rst index 26963759..f4863c9c 100644 --- a/docs/source/Zh/menu_tools.rst +++ b/docs/source/Zh/menu_tools.rst @@ -76,7 +76,7 @@ Diagram Editor Tab / Dock * - **New** - 清除目前圖形(畫布非空時會跳出確認)。 * - **Open** - - 載入先前儲存的 ``.diagram.json`` 檔案。 + - 載入先前儲存的 ``.diagram.json`` 檔案;目前的架構圖有沒存的變更時會先詢問。 * - **Save / Save As** (``Ctrl+S`` / ``Ctrl+Shift+S``) - 將圖形儲存為 ``.diagram.json``。 * - **Import Mermaid** diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index b34c8ba0..069ed2d8 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -577,3 +577,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py` - **Checks**: `test_unsaved_on_close.py` and `test_diagram_editor_widget.py` pass unchanged (27); `ruff` clean. - **Open items**: none. + +## U-20260925-68 · 2026-09-25 · Opening another diagram asks before unsaved changes go · #fix #diagram + +- **What**: the diagram editor's **Open** replaced the canvas and cleared the undo history with no question, so a diagram with unsaved changes was lost and could not be brought back by Undo. Closing the tab or the IDE already asked (U-20260923-164), and New asks before it discards a diagram; Open was the one way round it. Found while checking the Sphinx Tools page against the editor. +- **Change**: `_open_diagram` asks `diagram_editor_open_over_edits` ("The diagram has changes that are not saved. Open another and lose them?", No by default) through `_may_discard_edits` before the file dialog, only when the undo stack is not clean: a diagram as last saved or opened opens another unasked. The key is in both dictionaries (759 keys: the three READMEs and `architecture_explore.md` say so), and the Tools page, in both languages, says Open asks. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_unsaved_on_close.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md`, `docs/source/Eng/menu_tools.rst`, `docs/source/Zh/menu_tools.rst` +- **Checks**: three new tests in `test_unsaved_on_close.py`: No leaves the canvas, its path and its undo history alone without opening the file dialog (failed before the change: nothing was asked), Yes opens the other diagram, and a saved diagram opens another unasked. With the language parity, README key-count and diagram editor tests: all pass. `test/test_utils` collects 2592 tests; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index afa87e5c..0de3141d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-68 | 2026-09-25 | Opening another diagram asks before unsaved changes go | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-67 | 2026-09-25 | The diagram editor's unsaved-edits question in one place | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-66 | 2026-09-25 | The Sphinx Automation pages told right | #docs #menu | [2026-09-c](2026-09-c.md) | | U-20260925-65 | 2026-09-25 | The Sphinx Install pages list TestPioneer and the right fallback interpreter | #docs | [2026-09-c](2026-09-c.md) | @@ -470,4 +471,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 67 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 68 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 625749fc..ca2e76fc 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -238,6 +238,8 @@ "{filename} has edits that are not saved. Close and lose them?", "diagram_editor_close_over_edits": "The diagram has changes that are not saved. Close and lose them?", + "diagram_editor_open_over_edits": + "The diagram has changes that are not saved. Open another and lose them?", "unsaved_close_title": "Unsaved changes", "prompt_editor_reload_over_edits": "{filename} changed on disk. Reload it and lose the edits made here?", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index 96a34b70..c47e3884 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -229,6 +229,8 @@ "{filename} 還有沒存的編輯。要關閉並捨棄它們嗎?", "diagram_editor_close_over_edits": "架構圖還有沒存的變更。要關閉並捨棄它們嗎?", + "diagram_editor_open_over_edits": + "架構圖還有沒存的變更。要開啟另一個並捨棄它們嗎?", "unsaved_close_title": "尚未儲存的變更", "prompt_editor_reload_over_edits": "{filename} 在磁碟上被改過了。要重新載入並捨棄這裡還沒存的編輯嗎?", diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py index 2436f0ba..312b8a97 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py @@ -422,6 +422,11 @@ def _new_diagram(self) -> None: self._current_path = None def _open_diagram(self) -> None: + # Opening replaces the canvas and clears the undo history + if not self._may_discard_edits( + "diagram_editor_open_over_edits", + "The diagram has changes that are not saved. Open another and lose them?"): + return path, _ = QFileDialog.getOpenFileName( self, _lang("diagram_editor_dialog_open", "Open Diagram"), diff --git a/test/test_utils/test_unsaved_on_close.py b/test/test_utils/test_unsaved_on_close.py index 4284603d..1a16486d 100644 --- a/test/test_utils/test_unsaved_on_close.py +++ b/test/test_utils/test_unsaved_on_close.py @@ -1,4 +1,4 @@ -"""Closing a tab or the IDE asks before unsaved prompt or diagram edits are lost. +"""Closing a tab or the IDE, or opening another diagram, asks before unsaved edits are lost. JEditor's close_tab asked about its own editor tabs only and closed every other tab whatever it said, so a prompt or a diagram being edited was lost unasked. @@ -66,6 +66,59 @@ def test_a_saved_diagram_closes_without_a_question(self, app, answers, tmp_path) assert editor.may_close() is True assert answers["asked"] == 0 + @staticmethod + def _offer(monkeypatch, path): + from pybreeze.pybreeze_ui.diagram_editor import diagram_editor_widget + + chosen: list = [] + + def dialog(*_args, **_kwargs): + chosen.append(path) + return str(path), "" + + monkeypatch.setattr(diagram_editor_widget.QFileDialog, "getOpenFileName", staticmethod(dialog)) + return chosen + + def test_opening_another_diagram_asks_before_unsaved_changes_go(self, app, answers, tmp_path, monkeypatch): + # Open replaced the canvas and cleared the undo history: the edits were + # gone without a word and could not be undone. + other = tmp_path / "other.diagram.json" + self._editor()._write_json(other) + editor = self._editor() + editor._current_path = tmp_path / "work.diagram.json" + chosen = self._offer(monkeypatch, other) + + editor._open_diagram() + + assert answers["asked"] == 1 + assert chosen == [], "the file dialog opened after No" + assert editor._current_path == tmp_path / "work.diagram.json" + assert not editor._scene.undo_stack.isClean(), "the edits were dropped" + + def test_yes_lets_the_other_diagram_open(self, app, answers, tmp_path, monkeypatch): + other = tmp_path / "other.diagram.json" + self._editor()._write_json(other) + editor = self._editor() + answers["reply"] = QMessageBox.StandardButton.Yes + self._offer(monkeypatch, other) + + editor._open_diagram() + + assert answers["asked"] == 1 + assert editor._current_path == other + + def test_a_diagram_with_nothing_unsaved_opens_another_unasked(self, app, answers, tmp_path, monkeypatch): + other = tmp_path / "other.diagram.json" + self._editor()._write_json(other) + editor = self._editor() + editor._write_json(tmp_path / "saved.diagram.json") + self._offer(monkeypatch, other) + + editor._open_diagram() + + assert answers["asked"] == 0 + assert editor._current_path == other + class TestThePromptEditor: def test_unsaved_edits_are_asked_about(self, app, answers, tmp_path, monkeypatch): From b79d3e0d7c2d5ae528eb025a2f3f7a7a85131579 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 05:12:44 +0800 Subject: [PATCH 120/166] Correct the Sphinx Tools pages --- docs/source/Eng/menu_tools.rst | 221 ++++++++++++++++++++++----------- docs/source/Zh/menu_tools.rst | 182 ++++++++++++++++++--------- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- 4 files changed, 286 insertions(+), 128 deletions(-) diff --git a/docs/source/Eng/menu_tools.rst b/docs/source/Eng/menu_tools.rst index 90c4f6f7..5abaa5fa 100644 --- a/docs/source/Eng/menu_tools.rst +++ b/docs/source/Eng/menu_tools.rst @@ -1,112 +1,193 @@ Tools Menu ========== -The **Tools** menu provides access to the SSH client, AI-powered development -tools, and the built-in WYSIWYG architecture-diagram editor. Each tool can be -opened either as a **Tab** (in the main tab widget) or as a **Dock** (a -floating/dockable panel). +The **Tools** menu opens the SSH client, the AI tools, the built-in WYSIWYG +architecture-diagram editor and the HTTP / API utilities, each as a **Tab** in the +main tab widget. Each of them also opens as a **Dock** (a floating or dockable +panel) from the **Dock** menu: + +.. list-table:: + :header-rows: 1 + :widths: 25 40 35 + + * - Tool + - Tab + - Dock + * - SSH client + - **Tools > SSH > SSH Client Tab** + - **Dock > SSH > SSH Client Dock** + * - AI tools + - **Tools > AI >** ** **Tab** + - **Dock > AI >** ** **Dock** + * - Diagram editor + - **Tools > Diagram Editor Tab** + - **Dock > Diagram Editor Dock** + * - HTTP / API utilities + - **Tools >** ** **Tab** + - **Dock >** ** **Dock** SSH --- -SSH Client Tab -^^^^^^^^^^^^^^ - -Opens an SSH client interface as a new tab. See :doc:`ssh_client` for full details. - -SSH Client Dock -^^^^^^^^^^^^^^^ - -Opens the same SSH client as a dockable widget that can be positioned -around the edges of the main window. +**SSH Client Tab** opens an SSH client (a terminal and an SFTP file tree) as a new tab; +**SSH Client Dock** opens the same client as a dockable panel. See :doc:`ssh_client` +for full details. AI Tools -------- -AI Code Review Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Opens the AI Code Review client, which allows you to send code to an AI API -endpoint for automated code review. See :doc:`ai_tools` for full details. - -CoT Prompt Editor Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The **AI** submenus of **Tools** and **Dock** hold five tools. See :doc:`ai_tools` for full +details. -Opens the Chain-of-Thought (CoT) Prompt Editor for creating and managing -structured prompt templates. See :doc:`ai_tools` for full details. - -Skill Prompt Editor Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + :widths: 35 65 -Opens the Skill-based Prompt Editor for creating task-specific prompt templates -(e.g., code review prompts, code explanation prompts). See :doc:`ai_tools` for full details. + * - Tool + - Description + * - **AI Code Review** + - Sends code to an LLM endpoint for review, then accept or reject the suggestion. + * - **CoT Prompt Editor** + - Edits the Chain-of-Thought (CoT) review prompt templates. + * - **CoT Code Review** + - Runs the CoT review: each step's prompt goes to the endpoint in turn. + * - **Skill Prompt Editor** + - Edits the task-specific (skill) prompt templates, such as code review or code + explanation. + * - **Skill Send** + - Sends a skill prompt, with your code, to an LLM endpoint and shows the answer. + +HTTP and API Utilities +---------------------- + +Thirteen tools, each opened from **Tools** as a tab or from **Dock** as a dock. None of +them sends a request: they parse, convert and generate text. In a tool with one main +button, **Ctrl+Enter** anywhere in it presses that button (its text boxes take Enter as a +new line); in Query / JSON and the URL parser / builder, which convert both ways, it goes +the way the input reads: from JSON when the input is a JSON object. -Skill Send Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + :widths: 30 70 -Opens the Skill Prompt Sender interface for sending prompts to an LLM API -and viewing responses. See :doc:`ai_tools` for full details. + * - Tool + - Description + * - **cURL Import** + - Turns a ``curl`` command copied from a browser's dev tools into a Python + ``requests`` script, a pytest test, APITestka (Python or a JSON action list) or a + LoadDensity Locust load test. + * - **HAR Import** + - Lists the requests in a browser's HAR export and turns the ones selected into one + test script, with the same targets as cURL Import. + * - **JWT Decoder** + - Shows a token's header and payload, with its time claims in UTC. The signature is + never verified. + * - **Timestamp Converter** + - Takes a Unix epoch (seconds to nanoseconds) or an ISO-8601 date-time and gives + every representation in UTC. + * - **Hash Generator** + - SHA-256, SHA-512, SHA-1 and MD5 of the text at once. + * - **Query / JSON** + - ``application/x-www-form-urlencoded`` to JSON and back. + * - **URL Parser / Builder** + - A URL as an editable JSON object of its parts, and back again. + * - **Regex Tester** + - Every match with its offsets and groups, with the ``IGNORECASE``, ``MULTILINE``, + ``DOTALL`` and ``VERBOSE`` flags. + * - **HTTP Status Reference** + - The status code table, searched by code or keyword. + * - **Text Diff** + - A unified diff of two texts, with an added / removed summary. + * - **JSON Format** + - Pretty-prints or minifies JSON. + * - **HTTP Header Analyzer** + - Reports repeated headers, cookie flags, CORS, HSTS and CSP weaknesses, and missing + security headers. Headers carrying credentials are reported by name only. + * - **Response Inspector** + - Reads a pasted HTTP response: status, headers, a JSON body and any JWT in it, each + one a click away from its own tool. Diagram Editor -------------- -Diagram Editor Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Opens the built-in WYSIWYG architecture-diagram editor. Use it to sketch -flowcharts and architecture diagrams directly inside PyBreeze without -switching to an external tool. +**Diagram Editor Tab** / **Diagram Editor Dock** open the built-in WYSIWYG +architecture-diagram editor. Use it to sketch flowcharts and architecture diagrams +directly inside PyBreeze without switching to an external tool. Its keyboard shortcuts +apply only while it has the focus, so as a dock it leaves the code editor's own alone. Drawing tools """"""""""""" -- **Select** -- pick, move, multi-select with rubber-band -- **Rectangle / Rounded Rectangle / Ellipse / Diamond** -- node shapes -- **Connection** -- connect two nodes with a labelled line -- **Text** -- free-floating text annotation -- **Image (file)** -- insert a local image file -- **Image (URL)** -- download and insert an image from a URL (validated - against private/loopback IP ranges and capped at 20 MB to prevent SSRF) +The toolbar's first row: + +- **Select** -- click to select, drag to move, drag on the empty canvas to select with a + rubber band +- **Rect** / **Rounded** / **Ellipse** / **Diamond** -- click the canvas to place a node + of that shape; the tool then goes back to **Select** +- **Connect** -- click the source node, then the target node; a click on the empty canvas + or **Esc** cancels +- **Text** -- click the canvas to place a text node +- **Image** -- insert a local image file +- **URL Image** -- download and insert an image from an ``http`` / ``https`` URL. The + address is checked (private, loopback and other non-public addresses are refused), the + connection goes only to the address checked, and the download is capped at 20 MB and + 120 seconds; it runs in the background, so a slow host does not hold the IDE. + +Double-click a node to edit its text; the edit is one undo step. File operations """"""""""""""" +The second row starts with the file buttons: + .. list-table:: :header-rows: 1 :widths: 25 75 - * - Action + * - Button - Description * - **New** - - Discards the current diagram (with confirmation if non-empty). + - Clears the canvas, asking first when anything is on it. * - **Open** - Loads a previously saved ``.diagram.json`` file, asking first when the diagram - has changes that are not saved. - * - **Save** / **Save As** (``Ctrl+S`` / ``Ctrl+Shift+S``) - - Saves the diagram as ``.diagram.json``. - * - **Import Mermaid** - - Pastes Mermaid ``flowchart`` / ``graph`` source and converts it to - editable nodes and edges. - * - **Export PNG / SVG** - - Renders the canvas to a raster (PNG) or vector (SVG) image. + has changes that are not saved. A file that is not a diagram changes nothing. + * - **Save** (``Ctrl+S``) + - Saves to the file last opened or saved; the first time, it asks where, as + **Save As** does. + * - **Save As** (``Ctrl+Shift+S``) + - Saves the diagram as a new ``.diagram.json`` file. + * - **Import** + - Pastes Mermaid ``flowchart`` / ``graph`` source and converts it to editable, + automatically laid out nodes and connections. It replaces the canvas as one undo + step. + * - **PNG** / **SVG** + - Exports the canvas to a raster (PNG) or vector (SVG) image. + +Images in a saved diagram come back from where they were: a local path only if it is an +image file on this machine, and a URL through the same checks as **URL Image**. + +Closing the tab, the dock or the IDE with unsaved changes asks first as well. Editing helpers """"""""""""""" -- **Undo / Redo** (``Ctrl+Z`` / ``Ctrl+Y``) -- full undo stack with named - commands (Add, Move, Delete, Import, etc.) -- **Copy / Paste / Duplicate / Select All** -- standard shortcuts plus - ``Ctrl+D`` to duplicate selected items -- **Align** -- align selection by left, right, top, bottom, horizontal - centre, or vertical centre -- **Distribute** -- distribute three or more selected items evenly - horizontally or vertically -- **Grid** -- toggle background grid rendering -- **Snap** -- snap node positions to grid while dragging -- **Property Panel** (right side) -- edit text, colours, line width, - shape, and connection style of the selected item -- **Zoom** -- zoom in/out (``Ctrl+=`` / ``Ctrl+-``), reset to 100% - (``Ctrl+0``), or fit-to-content +- **Undo** / **Redo** (``Ctrl+Z`` / ``Ctrl+Y``) -- every change is one step +- **Delete** (or **Backspace**) removes the selection; ``Ctrl+C`` / ``Ctrl+V`` copy and + paste it, ``Ctrl+D`` duplicates it and ``Ctrl+A`` selects everything +- **Right-click** an item for **Delete**, **Duplicate** (nodes), **Bring to Front** and + **Send to Back**; the empty canvas for **Paste** (once something is copied) and + **Select All** +- **Align** -- **Align Left**, **Align Right**, **Align Top**, **Align Bottom**, + **Center Horizontal** and **Center Vertical** for the selected nodes, and + **Distribute Horizontal** / **Distribute Vertical** for three or more +- **Grid** -- shows the background grid +- **Snap** -- snaps nodes to the grid while dragging +- **Properties** panel (right side) -- the selected node's text, width, height, shape, + fill, border and font size; a connection's label, style (solid, dashed, dotted), + colour and width; an image's caption, width and height, and its source (read only) +- **Zoom** -- the mouse wheel, the **-** and **+** buttons or ``Ctrl+-`` / ``Ctrl+=``; + ``Ctrl+0`` resets to 100% and **Fit** shows the whole diagram. Drag with the right or + middle mouse button to pan. Tab vs. Dock ------------ diff --git a/docs/source/Zh/menu_tools.rst b/docs/source/Zh/menu_tools.rst index f4863c9c..2e639479 100644 --- a/docs/source/Zh/menu_tools.rst +++ b/docs/source/Zh/menu_tools.rst @@ -1,102 +1,170 @@ Tools 選單 ========== -**Tools** 選單提供 SSH 用戶端、AI 開發工具,以及內建的 WYSIWYG 架構圖編輯器。 -每個工具都可以作為 **分頁**\ (在主分頁元件中)或 **停靠面板**\ (浮動/可停靠面板)開啟。 +**Tools** 選單以 **分頁**\ (在主分頁元件中)開啟 SSH 用戶端、AI 工具、內建的 WYSIWYG 架構圖編輯器, +以及 HTTP / API 小工具。每一個也都能從 **Dock** 選單以 **停靠面板**\ (浮動/可停靠面板)開啟: + +.. list-table:: + :header-rows: 1 + :widths: 25 40 35 + + * - 工具 + - 分頁 + - 停靠面板 + * - SSH 用戶端 + - **Tools > SSH > SSH Client Tab** + - **Dock > SSH > SSH Client Dock** + * - AI 工具 + - **Tools > AI >** *<工具>* **Tab** + - **Dock > AI >** *<工具>* **Dock** + * - 架構圖編輯器 + - **Tools > Diagram Editor Tab** + - **Dock > Diagram Editor Dock** + * - HTTP / API 小工具 + - **Tools >** *<工具>* **Tab** + - **Dock >** *<工具>* **Dock** SSH --- -SSH Client Tab -^^^^^^^^^^^^^^ - -以新分頁的形式開啟 SSH 用戶端介面。詳細資訊請參閱 :doc:`ssh_client`。 - -SSH Client Dock -^^^^^^^^^^^^^^^ - -以可停靠面板的形式開啟相同的 SSH 用戶端,可以定位在主視窗的邊緣。 +**SSH Client Tab** 以新分頁開啟 SSH 用戶端(終端機與 SFTP 檔案樹);**SSH Client Dock** +以可停靠面板開啟同一個用戶端。詳細資訊請參閱 :doc:`ssh_client`。 AI 工具 ------- -AI Code Review Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Tools** 與 **Dock** 的 **AI** 子選單各有五個工具。詳細資訊請參閱 :doc:`ai_tools`。 -開啟 AI 程式碼審查用戶端,允許您將程式碼發送到 AI API 端點進行自動程式碼審查。 -詳細資訊請參閱 :doc:`ai_tools`。 - -CoT Prompt Editor Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -開啟思維鏈(CoT)提示詞編輯器,用於建立和管理結構化的提示詞範本。 -詳細資訊請參閱 :doc:`ai_tools`。 +.. list-table:: + :header-rows: 1 + :widths: 35 65 -Skill Prompt Editor Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * - 工具 + - 說明 + * - **AI Code Review** + - 把程式碼送到 LLM 端點審查,再接受或拒絕它的建議。 + * - **CoT Prompt Editor** + - 編輯思維鏈(CoT)審查的提示詞範本。 + * - **CoT Code Review** + - 執行 CoT 審查:每一步的提示詞依序送到端點。 + * - **Skill Prompt Editor** + - 編輯特定任務(技能)的提示詞範本,例如程式碼審查或程式碼解釋。 + * - **Skill Send** + - 把技能提示詞連同你的程式碼送到 LLM 端點,並顯示回答。 + +HTTP 與 API 小工具 +------------------ -開啟基於技能的提示詞編輯器,用於建立特定任務的提示詞範本 -(如程式碼審查提示詞、程式碼解釋提示詞)。詳細資訊請參閱 :doc:`ai_tools`。 +共十三個工具,都可以從 **Tools** 以分頁、或從 **Dock** 以停靠面板開啟。它們都不會送出請求, +只做解析、轉換與產生文字。在只有一個主要按鈕的工具裡,於任何位置按 **Ctrl+Enter** 就會按下那個按鈕 +(文字框裡的 Enter 是換行);Query / JSON 與 URL 解析/組建器可以雙向轉換,Ctrl+Enter 依輸入的內容決定方向: +輸入是 JSON 物件時從 JSON 轉回。 -Skill Send Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. list-table:: + :header-rows: 1 + :widths: 30 70 -開啟技能提示詞傳送介面,用於將提示詞發送到 LLM API 並查看回應。 -詳細資訊請參閱 :doc:`ai_tools`。 + * - 工具 + - 說明 + * - **cURL Import** + - 把從瀏覽器開發者工具複製的 ``curl`` 指令轉成 Python ``requests`` 腳本、pytest 測試、 + APITestka(Python 或 JSON 動作清單),或 LoadDensity 的 Locust 負載測試。 + * - **HAR Import** + - 列出瀏覽器 HAR 匯出檔裡的請求,把選取的請求轉成一份測試腳本,目標格式與 cURL Import 相同。 + * - **JWT Decoder** + - 顯示權杖的 header 與 payload,時間欄位以 UTC 呈現。不會驗證簽章。 + * - **Timestamp Converter** + - 輸入 Unix epoch(秒到奈秒)或 ISO-8601 日期時間,輸出所有 UTC 表示法。 + * - **Hash Generator** + - 同時算出文字的 SHA-256、SHA-512、SHA-1 與 MD5。 + * - **Query / JSON** + - ``application/x-www-form-urlencoded`` 與 JSON 互轉。 + * - **URL Parser / Builder** + - 把 URL 拆成可編輯的 JSON 物件,也能組回 URL。 + * - **Regex Tester** + - 列出每個符合項目的位置與群組,可用 ``IGNORECASE``、``MULTILINE``、``DOTALL``、 + ``VERBOSE`` 旗標。 + * - **HTTP Status Reference** + - 以狀態碼或關鍵字搜尋狀態碼表。 + * - **Text Diff** + - 兩段文字的 unified diff,附新增/刪除行數摘要。 + * - **JSON Format** + - 美化或壓縮 JSON。 + * - **HTTP Header Analyzer** + - 找出重複的標頭、Cookie 旗標、CORS、HSTS 與 CSP 的弱點,以及缺少的安全標頭。 + 帶有憑證的標頭只列出名稱。 + * - **Response Inspector** + - 解讀貼上的 HTTP 回應:狀態碼、標頭、JSON 本文與其中的 JWT,每一項都能一鍵在對應的工具中開啟。 架構圖編輯器 ------------ -Diagram Editor Tab / Dock -^^^^^^^^^^^^^^^^^^^^^^^^^ - -開啟內建的 WYSIWYG 架構圖編輯器,可直接在 PyBreeze 中繪製流程圖與架構圖, -不需要切換到外部工具。 +**Diagram Editor Tab** / **Diagram Editor Dock** 開啟內建的 WYSIWYG 架構圖編輯器, +可直接在 PyBreeze 中繪製流程圖與架構圖,不需要切換到外部工具。它的快捷鍵只在它取得焦點時作用, +所以作為停靠面板時不會搶走程式碼編輯器自己的快捷鍵。 繪圖工具 """""""" -- **選取(Select)** -- 點選、搬移、橡皮筋多選 -- **矩形 / 圓角矩形 / 橢圓 / 菱形** -- 節點形狀 -- **連線(Connection)** -- 將兩個節點以可加標籤的連線串接 -- **文字(Text)** -- 浮動文字註記 -- **圖片(檔案)** -- 插入本地圖片檔 -- **圖片(URL)** -- 從 URL 下載並插入圖片(會驗證 - 目標 IP 不在 private/loopback 範圍,並限制 20 MB 上限以防止 SSRF) +工具列的第一列: + +- **Select** -- 點選以選取、拖曳以搬移,在空白畫布上拖曳可用橡皮筋框選 +- **Rect** / **Rounded** / **Ellipse** / **Diamond** -- 點擊畫布放置該形狀的節點,之後工具回到 **Select** +- **Connect** -- 先點來源節點,再點目標節點;點空白畫布或按 **Esc** 取消 +- **Text** -- 點擊畫布放置文字節點 +- **Image** -- 插入本機圖片檔 +- **URL Image** -- 從 ``http`` / ``https`` URL 下載並插入圖片。會檢查位址(拒絕私有、loopback + 等非公開位址),只連線到檢查過的位址,下載上限為 20 MB 與 120 秒;在背景執行,主機回應慢也不會卡住 IDE。 + +雙擊節點可編輯它的文字;一次編輯是一個復原步驟。 檔案操作 """""""" +第二列開頭是檔案按鈕: + .. list-table:: :header-rows: 1 :widths: 25 75 - * - 操作 + * - 按鈕 - 說明 * - **New** - - 清除目前圖形(畫布非空時會跳出確認)。 + - 清空畫布;畫布上有東西時會先詢問。 * - **Open** - 載入先前儲存的 ``.diagram.json`` 檔案;目前的架構圖有沒存的變更時會先詢問。 - * - **Save / Save As** (``Ctrl+S`` / ``Ctrl+Shift+S``) - - 將圖形儲存為 ``.diagram.json``。 - * - **Import Mermaid** - - 貼上 Mermaid ``flowchart`` / ``graph`` 原始碼,轉換為可編輯的節點與邊。 - * - **Export PNG / SVG** + 不是架構圖的檔案不會改動任何東西。 + * - **Save**\ (``Ctrl+S``) + - 儲存到上次開啟或儲存的檔案;第一次儲存時會像 **Save As** 一樣詢問位置。 + * - **Save As**\ (``Ctrl+Shift+S``) + - 另存為新的 ``.diagram.json`` 檔案。 + * - **Import** + - 貼上 Mermaid ``flowchart`` / ``graph`` 原始碼,轉換為自動排版、可編輯的節點與連線。 + 它會取代整個畫布,並算作一個復原步驟。 + * - **PNG** / **SVG** - 將畫布輸出為點陣圖(PNG)或向量圖(SVG)。 +已儲存架構圖裡的圖片會從原處載回:本機路徑只接受這台電腦上的圖片檔,URL 則經過與 **URL Image** 相同的檢查。 + +有沒存的變更時,關閉分頁、停靠面板或 IDE 也都會先詢問。 + 編輯輔助 """""""" -- **Undo / Redo** (``Ctrl+Z`` / ``Ctrl+Y``) -- 完整的 undo stack,含具名指令 - (新增、搬移、刪除、Import 等) -- **Copy / Paste / Duplicate / Select All** -- 標準快捷鍵;``Ctrl+D`` 可複製選取項目 -- **對齊(Align)** -- 將選取項目以左、右、上、下、水平置中、垂直置中對齊 -- **分佈(Distribute)** -- 三個以上選取項目可水平或垂直平均分佈 -- **Grid** -- 切換背景格線 +- **Undo** / **Redo**\ (``Ctrl+Z`` / ``Ctrl+Y``)-- 每個變更都是一個步驟 +- **Delete**\ (或 **Backspace**)刪除選取項目;``Ctrl+C`` / ``Ctrl+V`` 複製與貼上, + ``Ctrl+D`` 複製一份,``Ctrl+A`` 全選 +- 在項目上 **按右鍵** 有 **Delete**、**Duplicate**\ (節點)、**Bring to Front**、**Send to Back**; + 在空白畫布上有 **Paste**\ (複製過東西之後)與 **Select All** +- **Align** -- 對選取的節點做 **Align Left**、**Align Right**、**Align Top**、**Align Bottom**、 + **Center Horizontal**、**Center Vertical**;選取三個以上時可 **Distribute Horizontal** / + **Distribute Vertical** +- **Grid** -- 顯示背景格線 - **Snap** -- 拖曳節點時對齊格線 -- **屬性面板(右側)** -- 編輯選取項目的文字、顏色、線寬、形狀、連線樣式 -- **Zoom** -- 放大/縮小 (``Ctrl+=`` / ``Ctrl+-``)、重設 100% - (``Ctrl+0``)、Fit-to-content +- **Properties** 面板(右側)-- 選取節點的文字、寬、高、形狀、填色、框線與字級;連線的標籤、 + 樣式(實線、虛線、點線)、顏色與寬度;圖片的說明、寬、高,以及它的來源(唯讀) +- **縮放** -- 滑鼠滾輪、**-** 與 **+** 按鈕,或 ``Ctrl+-`` / ``Ctrl+=``;``Ctrl+0`` 回到 100%, + **Fit** 顯示整張圖。按住滑鼠右鍵或中鍵拖曳可平移。 分頁 vs. 停靠面板 ------------------ diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 069ed2d8..9cf20470 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -585,3 +585,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_editor_widget.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_unsaved_on_close.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md`, `docs/source/Eng/menu_tools.rst`, `docs/source/Zh/menu_tools.rst` - **Checks**: three new tests in `test_unsaved_on_close.py`: No leaves the canvas, its path and its undo history alone without opening the file dialog (failed before the change: nothing was asked), Yes opens the other diagram, and a saved diagram opens another unasked. With the language parity, README key-count and diagram editor tests: all pass. `test/test_utils` collects 2592 tests; `ruff` clean. - **Open items**: none. + +## U-20260925-69 · 2026-09-25 · The Sphinx Tools pages told right · #docs #diagram + +- **What**: the audit found the Tools Menu page, in both languages, saying every tool opens as a Tab or a Dock from the Tools menu (the docks are in the Dock menu: Dock > SSH, Dock > AI, and the diagram editor and the utilities at its top level), without CoT Code Review among the AI tools, without the thirteen HTTP / API utilities (cURL Import to Response Inspector) at all, and with diagram editor labels that are not on its toolbar (Rectangle, Rounded Rectangle, Connection, Image (file), Image (URL), Import Mermaid, Export PNG / SVG; the buttons read Rect, Rounded, Connect, Image, URL Image, Import, PNG and SVG). It left out how a connection is drawn and cancelled, the right-click menus, Delete, Esc, the wheel zoom and the right- or middle-drag pan, that Save goes back to the file last opened or saved, that Import replaces the canvas as one undo step, what the property panel edits for an image, how a saved diagram's images come back, and that closing with unsaved changes asks. +- **Change**: both pages open with a table of where each tool's tab and dock are, list the five AI tools and the thirteen utilities (with the Ctrl+Enter rule), and describe the diagram editor from its two toolbar rows with the labels it shows. The Chinese page keeps the English labels, as the other Chinese pages do. +- **Files**: `docs/source/Eng/menu_tools.rst`, `docs/source/Zh/menu_tools.rst` +- **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and neither built page shows a stray `**` or double backquote. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0de3141d..0e8fab3e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-69 | 2026-09-25 | The Sphinx Tools pages told right | #docs #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-68 | 2026-09-25 | Opening another diagram asks before unsaved changes go | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-67 | 2026-09-25 | The diagram editor's unsaved-edits question in one place | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-66 | 2026-09-25 | The Sphinx Automation pages told right | #docs #menu | [2026-09-c](2026-09-c.md) | @@ -471,4 +472,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 68 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 69 | From d28b627ab0fba5969f3dd1b725d3e4045149eab6 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 12:26:01 +0800 Subject: [PATCH 121/166] Label the CoT review's code box as the code to review --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++++++ docs/updates/README.md | 3 ++- .../extend_multi_language/extend_english.py | 2 +- .../extend_traditional_chinese.py | 2 +- .../code_review/cot_code_review_gui.py | 2 +- test/test_utils/test_ai_gui_lifecycle.py | 24 +++++++++++++++++++ 7 files changed, 38 insertions(+), 5 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index b07b0e4a..90d2a196 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2592 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2594 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 9cf20470..939612cc 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -593,3 +593,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `docs/source/Eng/menu_tools.rst`, `docs/source/Zh/menu_tools.rst` - **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and neither built page shows a stray `**` or double backquote. - **Open items**: none. + +## U-20260925-70 · 2026-09-25 · CoT Code Review labels its code box as the code · #fix #ai + +- **What**: in CoT Code Review the box the code to review is pasted into was labelled "Prompt Area" (Chinese: 傳送資料區域, "area of data to send"), while its placeholder said "Paste the code to review here". The prompts are the eight templates of the CoT Prompt Editor; this box holds the code each of them quotes, so the label sent people looking for a prompt to type. Found while checking the Sphinx AI Tools page against the panel. +- **Change**: the label reads "Code to Review" / 要審查的程式碼 (the key `cot_gui_label_prompt_area` is kept, so a translation plugin that sets it still does). +- **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py` (its comment), `test/test_utils/test_ai_gui_lifecycle.py`, `architecture_explore.md` (test count) +- **Checks**: `TestCoTLabels` finds the new label in both languages and no label with "Prompt" or 傳送資料 (the English case failed with the old label); `test_ai_gui_lifecycle.py` and `test_language_parity.py` pass (37). `test/test_utils` collects 2594 tests; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0e8fab3e..3e7dc598 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-70 | 2026-09-25 | CoT Code Review labels its code box as the code | #fix #ai | [2026-09-c](2026-09-c.md) | | U-20260925-69 | 2026-09-25 | The Sphinx Tools pages told right | #docs #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-68 | 2026-09-25 | Opening another diagram asks before unsaved changes go | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-67 | 2026-09-25 | The diagram editor's unsaved-edits question in one place | #refactor #diagram | [2026-09-c](2026-09-c.md) | @@ -472,4 +473,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 69 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 70 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index ca2e76fc..681e16b5 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -369,7 +369,7 @@ "cot_gui_placeholder_code_paste_area": "Paste the code to review here", "cot_gui_label_step": "Step:", "cot_gui_placeholder_no_answers": "No answers yet", - "cot_gui_label_prompt_area": "Prompt Area", + "cot_gui_label_prompt_area": "Code to Review", "cot_gui_label_response_area": "Response Area", "cot_gui_button_send": "Start Sending", "cot_gui_warning_title": "Warning", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index c47e3884..d691eb5f 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -359,7 +359,7 @@ "cot_gui_placeholder_code_paste_area": "在這裡貼上要審查的程式碼", "cot_gui_label_step": "步驟:", "cot_gui_placeholder_no_answers": "還沒有回覆", - "cot_gui_label_prompt_area": "傳送資料區域", + "cot_gui_label_prompt_area": "要審查的程式碼", "cot_gui_label_response_area": "回傳區域", "cot_gui_button_send": "開始傳送", "cot_gui_warning_title": "警告", diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py index 41e0b848..4283b04b 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py @@ -31,7 +31,7 @@ def __init__(self): url_layout.addWidget(self.url_input) layout.addLayout(url_layout) - # 傳送資料區域 + # 要審查的程式碼 / The code to review; each step's prompt quotes it self.code_paste_area = QTextEdit() use_fixed_pitch_font(self.code_paste_area) self.code_paste_area.setPlaceholderText( diff --git a/test/test_utils/test_ai_gui_lifecycle.py b/test/test_utils/test_ai_gui_lifecycle.py index 3dd3cfff..d234e3b0 100644 --- a/test/test_utils/test_ai_gui_lifecycle.py +++ b/test/test_utils/test_ai_gui_lifecycle.py @@ -141,6 +141,30 @@ def test_the_warning_speaks_the_ide_language(self, qapp, monkeypatch): gui.deleteLater() +class TestCoTLabels: + @pytest.mark.parametrize("language", ["English", "Traditional_Chinese"]) + def test_the_box_for_the_code_is_labelled_as_the_code(self, qapp, monkeypatch, language): + # It read "Prompt Area": the prompts are the templates; this box holds + # the code each of them quotes + from pybreeze.extend_multi_language.extend_english import pybreeze_english_word_dict + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + from pybreeze.pybreeze_ui.extend_ai_gui.code_review import cot_code_review_gui as cot_mod + from PySide6.QtWidgets import QLabel + + word = {"English": pybreeze_english_word_dict, + "Traditional_Chinese": pybreeze_traditional_chinese_word_dict}[language] + monkeypatch.setattr(cot_mod.language_wrapper, "language_word_dict", word) + gui = CoTCodeReviewGUI() + + labels = [label.text() for label in gui.findChildren(QLabel)] + + assert {"English": "Code to Review", "Traditional_Chinese": "要審查的程式碼"}[language] in labels + assert not any("Prompt" in text or "傳送資料" in text for text in labels) + gui.deleteLater() + + class TestCoTWithoutCode: def test_nothing_is_sent_and_the_user_is_told(self, qapp, monkeypatch): # The whole chain, eight requests, ran on an empty box From a8e7021c6024dcf53f02aac27fa3e941e6ea977c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 12:27:51 +0800 Subject: [PATCH 122/166] Correct the Sphinx AI Tools pages --- docs/source/Eng/ai_tools.rst | 235 +++++++++++++++++++---------------- docs/source/Zh/ai_tools.rst | 222 +++++++++++++++++---------------- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- 4 files changed, 254 insertions(+), 214 deletions(-) diff --git a/docs/source/Eng/ai_tools.rst b/docs/source/Eng/ai_tools.rst index 4b20a57f..92077ae7 100644 --- a/docs/source/Eng/ai_tools.rst +++ b/docs/source/Eng/ai_tools.rst @@ -1,155 +1,180 @@ AI Tools ======== -PyBreeze integrates several AI-powered tools for code review, prompt engineering, -and LLM interaction. All AI tools are accessible from the **Tools** menu and can -be opened as either tabs or dock widgets. +PyBreeze has five tools for AI-assisted code review and prompt work. Each opens as a +tab from **Tools > AI** or as a dock from **Dock > AI**. The code review of a file or a +pull request by prthinker is in the **Automation** menu instead (see +:doc:`menu_automation`). + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Tool + - What it does + * - **AI Code Review** + - Sends code to an endpoint in one request; accept or reject the answer. + * - **CoT Code Review** + - Runs the eight-step Chain-of-Thought (CoT) review: one request per step. + * - **CoT Prompt Editor** + - Edits the eight CoT prompts. + * - **Skill Prompt Editor** + - Edits the two skill prompts (code review, code explanation). + * - **Skill Send** + - Sends one skill prompt, with your code in it, and shows the answer. + +In AI Code Review, CoT Code Review and Skill Send, **Ctrl+Enter** anywhere in the panel +presses its send button. The request runs in the background, and the send button stays +greyed out until it ends. -AI Code Review Client ---------------------- +.. note:: + + The endpoint URL must be a public ``http`` / ``https`` address. It is checked before + anything is sent, and the connection goes only to the address checked: an endpoint on + this machine or on a private network, such as a local model server, is refused. + Redirects are not followed, the answer is capped at 16 MB, and a request that runs + past five minutes is stopped. Only AI Code Review records the URLs it used, and only as + fingerprints (see below), since an API URL can carry a token. -**Menu:** Tools > AI Code Review Tab / AI Code Review Dock +AI Code Review +-------------- -A client for sending code to an AI API endpoint for automated code review. +**Menu:** Tools > AI > AI Code Review Tab / Dock > AI > AI Code Review Dock Interface Layout ^^^^^^^^^^^^^^^^ -- **URL Input** -- Enter the API endpoint URL -- **Method Selector** -- Choose HTTP method (GET, POST, PUT, DELETE) -- **Code Input** (left panel) -- Paste or write code to be reviewed -- **Response Display** (right panel, read-only) -- Shows the AI review response -- **Send Request** button -- Sends the code to the API endpoint - -Features -^^^^^^^^ - -- Tracks accept/reject statistics for AI responses -- Records which endpoints have been used in ``.pybreeze/urls.txt``, as fingerprints: - an API URL can carry a token, so the URL itself is never written to disk -- Stores response statistics in ``.pybreeze/response_stats.txt`` +- **URL** -- the endpoint URL +- **Method** -- ``GET``, ``POST`` (the default), ``PUT`` or ``DELETE`` +- **Code to Send** (left) -- the code to review +- **Response** (right, read-only) -- the answer, or why there is none +- **Send Request** -- sends the request +- **Accept Response** / **Reject Response** -- your verdict on the answer Usage ^^^^^ -1. Enter your AI API endpoint URL in the URL input field -2. Select the HTTP method (typically POST) -3. Paste the code you want reviewed in the left panel -4. Click **Send Request** -5. Review the AI's response in the right panel +1. Enter the endpoint URL and choose the method. ``POST`` and ``PUT`` send the code as + the form field ``code`` in the body; ``GET`` and ``DELETE`` send the URL alone. +2. Paste the code to review on the left (``POST`` and ``PUT`` need some). +3. Click **Send Request**. The response panel first says whether this URL has been used + before, then shows the answer when it arrives. An answer that is not a success (an + HTTP error, or a redirect, which is not followed) is shown as an error with its status. +4. Click **Accept Response** or **Reject Response**. They are enabled once an answer has + arrived, and take one verdict per answer. -CoT Code Review GUI --------------------- +Files +^^^^^ + +- ``~/.pybreeze/response_stats.txt`` -- the running totals of accepted and rejected + answers, shared by every AI Code Review panel +- ``~/.pybreeze/urls.txt`` -- the URLs used, as SHA-256 fingerprints: the URL itself is + never written to disk -**Menu:** Tools > AI Code Review Tab / Dock +CoT Code Review +--------------- -An advanced code review tool using Chain-of-Thought (CoT) prompting for -more structured and detailed reviews. +**Menu:** Tools > AI > CoT Code Review Tab / Dock > AI > CoT Code Review Dock Interface Layout ^^^^^^^^^^^^^^^^ -- **API URL Input** -- Enter the API endpoint URL -- **Code Area** -- Paste code for review -- **Response Selector** (ComboBox) -- Browse through multiple review responses -- **Response Viewer** (read-only) -- Displays the selected review response -- **Send Button** -- Sends code for review +- **API URL** -- the endpoint URL +- **Code to Review** -- the code the review is about +- **Response Area** -- **Step** selector (each step whose answer has arrived) beside the + selected step's answer (read-only) +- **Start Sending** -- runs the review -Features -^^^^^^^^ +How a review runs +^^^^^^^^^^^^^^^^^ -- Supports reviewing multiple files at once via ``SenderThread`` -- Background threading prevents UI freezing during API calls -- Multiple responses can be stored and browsed +The eight steps run in this order, because each may quote the answers of the steps +before it: + +1. ``first_summary_prompt.md`` -- a first summary of the code +2. ``first_code_review.md`` -- a first review +3. ``judge_single_review.md`` -- a judge of that review +4. ``linter.md`` -- lint findings +5. ``code_smell_detector.md`` -- code smells +6. ``step_by_step_analysis.md`` -- each lint finding and code smell walked through +7. ``total_summary.md`` -- the summary of everything above +8. ``judge.md`` -- a judge of the summary + +Each step's prompt, wrapped in the global review rules, is sent as a ``POST`` of the JSON +``{"prompt": "..."}``, and the response body, as text, is that step's answer. It appears +under **Step** as it arrives and is shown at once. A step that fails shows why, and the +steps after it do not quote the failure. **Start Sending** clears the previous run's +answers, and closing the panel stops the review after the request in flight. + +The prompts are the CoT Prompt Editor's: an edited prompt is used in place of the +built-in one. CoT Prompt Editor ----------------- -**Menu:** Tools > CoT Prompt Editor Tab / CoT Prompt Editor Dock - -A template-based editor for creating and managing Chain-of-Thought prompt templates. +**Menu:** Tools > AI > CoT Prompt Editor Tab / Dock > AI > CoT Prompt Editor Dock Interface Layout ^^^^^^^^^^^^^^^^ -- **File Selector** (ComboBox) -- Select from available prompt template files -- **Edit Panel** (QTextEdit) -- Edit the selected prompt template -- **Create** button -- Creates a new prompt template file -- **Save** button -- Saves changes to the current template -- **Reload** button -- Reloads the template from disk +- **Edit File Content** -- the text of the prompt chosen below +- The folder the prompt files are kept in, ``~/.pybreeze/prompts/`` +- The prompt selector (one entry per step, as listed above) +- **Reload** -- reads the file again from disk +- **Save** -- writes the text to the file +- **Create File** -- creates the file from the built-in prompt -Features -^^^^^^^^ +How prompts are kept +^^^^^^^^^^^^^^^^^^^^ -- Template-based file management with ``COT_TEMPLATE_RELATION`` mapping -- File system watcher for detecting external changes -- Auto-reloads templates when modified outside the editor -- Pre-configured templates for common CoT review patterns +Every prompt is built in. A file of the same name in ``~/.pybreeze/prompts/`` replaces it +while that file has content, so a review sends what you saved. Until the file exists, the +edit area is empty and says so; **Create File** writes the built-in prompt into it as a +starting point. -Usage -^^^^^ +A prompt's placeholders, such as ``{code_diff}``, are filled in when the review runs. An +edited prompt that names a placeholder the step cannot fill falls back to the built-in +one for that run. -1. Select a template from the dropdown or create a new one -2. Edit the prompt template in the text area -3. Click **Save** to persist your changes -4. The template can then be used in the CoT Code Review GUI +The files are watched: an edit made outside the editor shows up at once. Whenever +showing another text would lose unsaved edits -- choosing another prompt, **Reload**, +**Create File**, an outside change, or closing the tab, the dock or the IDE -- the editor +asks first, with **No** as the default. A file that is not UTF-8 is shown with what cannot +be read replaced, and says so; saving writes it back as UTF-8. Skill Prompt Editor ------------------- -**Menu:** Tools > Skill Prompt Editor Tab / Skill Prompt Editor Dock +**Menu:** Tools > AI > Skill Prompt Editor Tab / Dock > AI > Skill Prompt Editor Dock -Similar to the CoT Prompt Editor, but specialized for skill-based prompt templates -such as code review and code explanation prompts. +The same editor as the CoT Prompt Editor, for the two skill prompts: +``code_review_skill.md`` (a code review) and ``code_explainer_skill.md`` (a code +explanation). Its files are kept in the same folder and work the same way. -Interface Layout -^^^^^^^^^^^^^^^^ - -- **File Selector** (ComboBox) -- Select from available skill prompt templates -- **Edit Panel** (QTextEdit) -- Edit the selected skill prompt -- **Create** button -- Creates a new skill prompt template -- **Save** button -- Saves changes -- **Reload** button -- Reloads from disk - -Pre-built Skill Templates -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- Code Review prompts -- Code Explanation prompts - -Skills Send GUI ---------------- +Skill Send +---------- -**Menu:** Tools > Skill Send Tab / Skill Send Dock - -An interface for sending skill-based prompts to an LLM API and viewing responses. +**Menu:** Tools > AI > Skill Send Tab / Dock > AI > Skill Send Dock Interface Layout ^^^^^^^^^^^^^^^^ -- **API URL Input** -- Enter the LLM API endpoint URL -- **Prompt Template Selector** (ComboBox) -- Choose a pre-defined skill prompt template -- **Prompt Text Area** -- Edit or customize the prompt before sending -- **Send Button** -- Sends the prompt to the API (runs in background thread) -- **Response Display** (read-only) -- Shows the LLM response - -Features -^^^^^^^^ - -- Background threading via ``RequestThread`` prevents UI freezing -- Error handling with specific HTTP status code messages -- Prompt templates are loaded from the Skill Prompt Editor's template files +- **LLM API URL** -- the endpoint URL +- **Select Prompt Template** -- the skill prompt to start from +- **Prompt** -- the prompt that is sent, editable +- **Send** -- sends it +- **Response** (read-only) -- the answer, or why there is none Usage ^^^^^ -1. Enter your LLM API endpoint URL -2. Select a prompt template from the dropdown -3. Customize the prompt text if needed (e.g., paste code to review) -4. Click **Send** -5. Wait for the response to appear in the response display area - -.. note:: - - All AI tools require a compatible API endpoint. Configure your API URL - to point to your LLM service (e.g., OpenAI-compatible API, local LLM server, etc.). +1. Enter the endpoint URL. +2. Choose a template. Its text (the edited file if there is one, otherwise the built-in + prompt) fills **Prompt**. Choosing another template after editing the prompt asks + first. +3. Put your code in place of ``{code_diff}`` in the prompt. The prompt is not sent while + ``{code_diff}`` is still in it. +4. Click **Send**. The prompt goes as a ``POST`` of the JSON ``{"code": "..."}``, and the + response body is shown as it is. A refused request (401, 403) and a server error are + shown as errors; a redirect is not followed and says where it pointed (its scheme and + host only). diff --git a/docs/source/Zh/ai_tools.rst b/docs/source/Zh/ai_tools.rst index 617027f4..2d87166b 100644 --- a/docs/source/Zh/ai_tools.rst +++ b/docs/source/Zh/ai_tools.rst @@ -1,153 +1,159 @@ AI 工具 ======= -PyBreeze 整合了多個 AI 驅動的工具,用於程式碼審查、提示詞工程和 LLM 互動。 -所有 AI 工具都可以從 **Tools** 選單存取,並可以作為分頁或停靠面板開啟。 +PyBreeze 有五個 AI 輔助程式碼審查與提示詞的工具,都可以從 **Tools > AI** 以分頁開啟,或從 +**Dock > AI** 以停靠面板開啟。用 prthinker 審查檔案或 Pull Request 則在 **Automation** 選單 +(見 :doc:`menu_automation`)。 + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - 工具 + - 用途 + * - **AI Code Review** + - 以一次請求把程式碼送到端點審查,再接受或拒絕回答。 + * - **CoT Code Review** + - 執行八個步驟的思維鏈(CoT)審查:每個步驟一次請求。 + * - **CoT Prompt Editor** + - 編輯八個 CoT 提示詞。 + * - **Skill Prompt Editor** + - 編輯兩個技能提示詞(程式碼審查、程式碼解釋)。 + * - **Skill Send** + - 送出一個放入你程式碼的技能提示詞,並顯示回答。 + +在 AI Code Review、CoT Code Review 與 Skill Send 中,於面板任何位置按 **Ctrl+Enter** 就會按下送出按鈕。 +請求在背景執行,送出按鈕在請求結束前會維持停用。 -AI 程式碼審查用戶端 --------------------- - -**選單:** Tools > AI Code Review Tab / AI Code Review Dock - -用於將程式碼發送到 AI API 端點進行自動程式碼審查的用戶端。 - -介面配置 -^^^^^^^^ +.. note:: -- **URL 輸入框** -- 輸入 API 端點 URL -- **方法選擇器** -- 選擇 HTTP 方法(GET、POST、PUT、DELETE) -- **程式碼輸入區**\ (左面板)-- 貼上或撰寫要審查的程式碼 -- **回應顯示區**\ (右面板,唯讀)-- 顯示 AI 審查回應 -- **Send Request** 按鈕 -- 將程式碼發送到 API 端點 + 端點 URL 必須是公開的 ``http`` / ``https`` 位址。送出前會先檢查,而且只連線到檢查過的位址: + 位於本機或私有網路的端點(例如本機的模型伺服器)會被拒絕。不會跟隨重新導向,回答上限為 16 MB, + 超過五分鐘的請求會被中止。只有 AI Code Review 會記錄用過的 URL,而且只記指紋(見下文), + 因為 API URL 可能夾帶權杖。 -功能 -^^^^ +AI Code Review +-------------- -- 追蹤 AI 回應的接受/拒絕統計 -- 在 ``.pybreeze/urls.txt`` 記錄用過哪些端點,存的是指紋:API URL 可能帶著權杖, - 所以不會把 URL 本身寫進磁碟 -- 將回應統計儲存到 ``.pybreeze/response_stats.txt`` +**選單:** Tools > AI > AI Code Review Tab/Dock > AI > AI Code Review Dock -使用方式 +介面佈局 ^^^^^^^^ -1. 在 URL 輸入框中輸入您的 AI API 端點 URL -2. 選擇 HTTP 方法(通常為 POST) -3. 在左面板中貼上要審查的程式碼 -4. 點擊 **Send Request** -5. 在右面板中查看 AI 的回應 +- **URL** -- 端點 URL +- **Method** -- ``GET``、``POST``\ (預設)、``PUT`` 或 ``DELETE`` +- **Code to Send**\ (左側)-- 要審查的程式碼 +- **Response**\ (右側,唯讀)-- 回答,或沒有回答的原因 +- **Send Request** -- 送出請求 +- **Accept Response** / **Reject Response** -- 你對回答的評價 -CoT 程式碼審查 GUI --------------------- - -**選單:** Tools > AI Code Review Tab / Dock - -使用思維鏈(CoT)提示詞進行更結構化和詳細審查的進階程式碼審查工具。 - -介面配置 +使用方式 ^^^^^^^^ -- **API URL 輸入框** -- 輸入 API 端點 URL -- **程式碼區域** -- 貼上要審查的程式碼 -- **回應選擇器**\ (ComboBox)-- 瀏覽多個審查回應 -- **回應檢視器**\ (唯讀)-- 顯示選定的審查回應 -- **Send** 按鈕 -- 發送程式碼進行審查 +1. 輸入端點 URL 並選擇方法。``POST`` 與 ``PUT`` 會把程式碼放在本文的表單欄位 ``code``; + ``GET`` 與 ``DELETE`` 只送 URL。 +2. 在左側貼上要審查的程式碼(``POST`` 與 ``PUT`` 必須有內容)。 +3. 點擊 **Send Request**。回應區先說明這個 URL 是否用過,回答到達後再顯示回答。 + 不是成功的回答(HTTP 錯誤,或不會跟隨的重新導向)會連同狀態碼以錯誤顯示。 +4. 點擊 **Accept Response** 或 **Reject Response**。兩者在回答到達後才能按,每個回答只能評一次。 -功能 +檔案 ^^^^ -- 透過 ``SenderThread`` 支援一次審查多個檔案 -- 背景執行緒防止 API 呼叫時 UI 凍結 -- 可以儲存和瀏覽多個回應 +- ``~/.pybreeze/response_stats.txt`` -- 接受與拒絕的累計次數,所有 AI Code Review 面板共用 +- ``~/.pybreeze/urls.txt`` -- 用過的 URL 的 SHA-256 指紋:URL 本身從不寫入磁碟 -CoT 提示詞編輯器 ------------------ - -**選單:** Tools > CoT Prompt Editor Tab / CoT Prompt Editor Dock +CoT Code Review +--------------- -基於範本的編輯器,用於建立和管理思維鏈提示詞範本。 +**選單:** Tools > AI > CoT Code Review Tab/Dock > AI > CoT Code Review Dock -介面配置 +介面佈局 ^^^^^^^^ -- **檔案選擇器**\ (ComboBox)-- 從可用的提示詞範本檔案中選擇 -- **編輯面板**\ (QTextEdit)-- 編輯選定的提示詞範本 -- **Create** 按鈕 -- 建立新的提示詞範本檔案 -- **Save** 按鈕 -- 儲存目前範本的變更 -- **Reload** 按鈕 -- 從磁碟重新載入範本 +- **API URL** -- 端點 URL +- **Code to Review** -- 要審查的程式碼 +- **Response Area** -- **Step** 選單(列出已收到回答的步驟),旁邊顯示所選步驟的回答(唯讀) +- **Start Sending** -- 開始審查 -功能 -^^^^ +審查如何進行 +^^^^^^^^^^^^ -- 透過 ``COT_TEMPLATE_RELATION`` 對應的範本檔案管理 -- 檔案系統監控,偵測外部變更 -- 當範本在編輯器外修改時自動重新載入 -- 預先設定的常用 CoT 審查模式範本 +八個步驟依下列順序執行,因為每一步都可能引用前面步驟的回答: -使用方式 -^^^^^^^^ +1. ``first_summary_prompt.md`` -- 程式碼的初步摘要 +2. ``first_code_review.md`` -- 初步審查 +3. ``judge_single_review.md`` -- 評審這份審查 +4. ``linter.md`` -- lint 發現 +5. ``code_smell_detector.md`` -- 程式碼異味 +6. ``step_by_step_analysis.md`` -- 逐一分析每個 lint 發現與程式碼異味 +7. ``total_summary.md`` -- 以上所有內容的總結 +8. ``judge.md`` -- 評審這份總結 -1. 從下拉選單選擇範本或建立新範本 -2. 在文字區域中編輯提示詞範本 -3. 點擊 **Save** 儲存變更 -4. 該範本可以在 CoT 程式碼審查 GUI 中使用 +每一步的提示詞會包上全域審查規則,以 JSON ``{"prompt": "..."}`` 的 ``POST`` 送出,回應本文(文字) +就是該步驟的回答。回答一到就出現在 **Step** 選單並立即顯示。失敗的步驟會顯示原因,之後的步驟不會引用這個失敗。 +**Start Sending** 會清掉上一次的回答;關閉面板時,審查會在進行中的請求結束後停止。 -Skill 提示詞編輯器 -------------------- +提示詞來自 CoT Prompt Editor:編輯過的提示詞會取代內建的。 -**選單:** Tools > Skill Prompt Editor Tab / Skill Prompt Editor Dock +CoT Prompt Editor +----------------- -與 CoT 提示詞編輯器類似,但專門用於基於技能的提示詞範本, -如程式碼審查和程式碼解釋提示詞。 +**選單:** Tools > AI > CoT Prompt Editor Tab/Dock > AI > CoT Prompt Editor Dock -介面配置 +介面佈局 ^^^^^^^^ -- **檔案選擇器**\ (ComboBox)-- 從可用的技能提示詞範本中選擇 -- **編輯面板**\ (QTextEdit)-- 編輯選定的技能提示詞 -- **Create** 按鈕 -- 建立新的技能提示詞範本 -- **Save** 按鈕 -- 儲存變更 -- **Reload** 按鈕 -- 從磁碟重新載入 +- **Edit File Content** -- 下方所選提示詞的內容 +- 提示詞檔案所在的資料夾 ``~/.pybreeze/prompts/`` +- 提示詞選單(每個步驟一項,如上表) +- **Reload** -- 從磁碟重新讀取檔案 +- **Save** -- 把內容寫入檔案 +- **Create File** -- 以內建提示詞建立檔案 -預設技能範本 -^^^^^^^^^^^^ +提示詞如何保存 +^^^^^^^^^^^^^^ -- 程式碼審查提示詞 -- 程式碼解釋提示詞 +每個提示詞都有內建版本。``~/.pybreeze/prompts/`` 中同名的檔案只要有內容就會取代它, +所以審查送出的是你存下的內容。檔案建立之前,編輯區是空的並會註明;**Create File** 會把內建提示詞寫進檔案, +當作起點。 -Skills 傳送 GUI ----------------- +提示詞中的佔位符(例如 ``{code_diff}``)會在審查執行時填入。編輯過的提示詞若用了該步驟填不了的佔位符, +那一次會改用內建提示詞。 -**選單:** Tools > Skill Send Tab / Skill Send Dock +檔案會被監看:在編輯器外的修改會立刻出現。只要換上另一份內容會失去沒存的編輯——選另一個提示詞、 +**Reload**、**Create File**、外部修改,或關閉分頁、停靠面板或 IDE——編輯器都會先詢問,預設為 **No**。 +不是 UTF-8 的檔案會把讀不出的部分替換後顯示並告知;存檔時會寫回 UTF-8。 -用於將基於技能的提示詞發送到 LLM API 並查看回應的介面。 +Skill Prompt Editor +------------------- -介面配置 -^^^^^^^^ +**選單:** Tools > AI > Skill Prompt Editor Tab/Dock > AI > Skill Prompt Editor Dock -- **API URL 輸入框** -- 輸入 LLM API 端點 URL -- **提示詞範本選擇器**\ (ComboBox)-- 選擇預定義的技能提示詞範本 -- **提示詞文字區域** -- 在發送前編輯或自訂提示詞 -- **Send** 按鈕 -- 將提示詞發送到 API(在背景執行緒中執行) -- **回應顯示區**\ (唯讀)-- 顯示 LLM 回應 +與 CoT Prompt Editor 相同的編輯器,用於兩個技能提示詞:``code_review_skill.md``\ (程式碼審查)與 +``code_explainer_skill.md``\ (程式碼解釋)。檔案放在同一個資料夾,運作方式也相同。 -功能 -^^^^ +Skill Send +---------- -- 透過 ``RequestThread`` 在背景執行緒中執行,防止 UI 凍結 -- 具備特定 HTTP 狀態碼訊息的錯誤處理 -- 從 Skill 提示詞編輯器的範本檔案載入提示詞範本 +**選單:** Tools > AI > Skill Send Tab/Dock > AI > Skill Send Dock -使用方式 +介面佈局 ^^^^^^^^ -1. 輸入您的 LLM API 端點 URL -2. 從下拉選單選擇提示詞範本 -3. 根據需要自訂提示詞文字(例如,貼上要審查的程式碼) -4. 點擊 **Send** -5. 等待回應出現在回應顯示區域 +- **LLM API URL** -- 端點 URL +- **Select Prompt Template** -- 作為起點的技能提示詞 +- **Prompt** -- 要送出的提示詞,可編輯 +- **Send** -- 送出 +- **Response**\ (唯讀)-- 回答,或沒有回答的原因 -.. note:: +使用方式 +^^^^^^^^ - 所有 AI 工具都需要相容的 API 端點。請將您的 API URL 指向您的 LLM 服務 - (例如 OpenAI 相容的 API、本地 LLM 伺服器等)。 +1. 輸入端點 URL。 +2. 選擇範本。它的內容(有編輯過的檔案就用檔案,否則用內建提示詞)會填入 **Prompt**。 + 編輯過提示詞後再選另一個範本時會先詢問。 +3. 把提示詞中的 ``{code_diff}`` 換成你的程式碼。提示詞裡還有 ``{code_diff}`` 時不會送出。 +4. 點擊 **Send**。提示詞以 JSON ``{"code": "..."}`` 的 ``POST`` 送出,回應本文原樣顯示。 + 被拒絕的請求(401、403)與伺服器錯誤會以錯誤顯示;重新導向不會被跟隨,並說明它指向哪裡 + (只列出 scheme 與主機)。 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 939612cc..ddbc5544 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -601,3 +601,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py` (its comment), `test/test_utils/test_ai_gui_lifecycle.py`, `architecture_explore.md` (test count) - **Checks**: `TestCoTLabels` finds the new label in both languages and no label with "Prompt" or 傳送資料 (the English case failed with the old label); `test_ai_gui_lifecycle.py` and `test_language_parity.py` pass (37). `test/test_utils` collects 2594 tests; `ruff` clean. - **Open items**: none. + +## U-20260925-71 · 2026-09-25 · The Sphinx AI Tools pages told right · #docs #ai + +- **What**: the audit found the AI Tools page, in both languages, with menu paths that are not there (Tools > AI Code Review Tab, and for CoT Code Review the AI Code Review entry; the entries are under Tools > AI and Dock > AI), labels the panels do not show (URL Input, Method Selector, Code Area, Send Button, Response Selector; they read URL, Method, Code to Send, API URL, Code to Review, Step, Start Sending, LLM API URL, Select Prompt Template), no Accept / Reject buttons, the data files at `.pybreeze/` in the working folder (they are under `~/.pybreeze/`), CoT Code Review "reviewing multiple files at once" (it reviews the pasted code in eight steps, one request each), a Create button (Create File) and an editor that "auto-reloads" over unsaved edits (it asks), and a note pointing at a local LLM server, which the URL check refuses. +- **Change**: both pages list the five tools with their real paths and labels, say what each request sends (`code` form field, `{"prompt": ...}`, `{"code": ...}`), the eight CoT steps in order and why, where prompts are kept and which wins, when the editors ask, that Skill Send will not send while `{code_diff}` is in the prompt, the files AI Code Review keeps, and what the URL check allows (public addresses only, no redirects, 16 MB, five minutes). +- **Files**: `docs/source/Eng/ai_tools.rst`, `docs/source/Zh/ai_tools.rst` +- **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and neither built page shows a stray `**` or double backquote. +- **Open items**: none (whether a local model server should be allowed is `progress.md` #111). diff --git a/docs/updates/README.md b/docs/updates/README.md index 3e7dc598..a4ca5de5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-71 | 2026-09-25 | The Sphinx AI Tools pages told right | #docs #ai | [2026-09-c](2026-09-c.md) | | U-20260925-70 | 2026-09-25 | CoT Code Review labels its code box as the code | #fix #ai | [2026-09-c](2026-09-c.md) | | U-20260925-69 | 2026-09-25 | The Sphinx Tools pages told right | #docs #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-68 | 2026-09-25 | Opening another diagram asks before unsaved changes go | #fix #diagram | [2026-09-c](2026-09-c.md) | @@ -473,4 +474,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 70 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 71 | From 1777197a1a4443e4d5e13fc5226b00e4a5adb318 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 12:33:22 +0800 Subject: [PATCH 123/166] Offer the Plugin Browser when no plugin is loaded --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 4 ++-- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- .../menu/plugin_menu/build_plugin_menu.py | 11 ++++++----- test/test_utils/test_plugin_menu.py | 17 +++++++++++++++-- 8 files changed, 36 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a37fb720..e1306cd0 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ PyBreeze inherits JEditor's plugin architecture, auto-discovered from a `jeditor - **Syntax highlighting** — keyword sets and rules for any language - **UI translations** — new interface languages - **Run configurations** — "Run with…" for interpreted (`go run main.go`) and compiled (`gcc main.c -o main` then run) languages, executed through PyBreeze's `FileRunnerProcess` with the compiled artifact cleaned up afterwards -- **Plugin Browser** — browse and install plugins from remote repositories inside the IDE +- **Plugin Browser** — browse and install plugins from remote repositories inside the IDE, from **Plugins → Plugin Browser**, which is there before any plugin is installed; an installed plugin loads at the next start Loaded plugins appear under their own **Plugins** menu with an About entry and one run action, labelled with the suffixes it runs. [PLUGIN_GUIDE.md](PLUGIN_GUIDE.md) covers what PyBreeze adds and links JEditor's guide, which has the full API and worked examples (C, C++, Go, Java, Rust, and a French translation). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 12e2bb62..3e15e654 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -204,7 +204,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **语法高亮** — 任何语言的关键字集与规则 - **界面翻译** — 新的界面语言 - **运行配置** — 为解释型(`go run main.go`)与编译型(`gcc main.c -o main` 后再运行)语言提供"Run with…",通过 PyBreeze 的 `FileRunnerProcess` 执行,编译产物在运行后会被清理 -- **插件浏览器** — 在 IDE 内从远程仓库浏览并安装插件 +- **插件浏览器** — 在 IDE 内从远程仓库浏览并安装插件,入口是 **Plugins → Plugin Browser**,尚未安装任何插件时也在;装好的插件在下次启动时加载 已加载的插件会出现在它们自己的 **Plugins** 菜单下,包含一个 About 项与一个运行动作,标签上注明它能运行的文件扩展名。[PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md) 介绍 PyBreeze 额外提供的内容,并链接到 JEditor 的指南,那里有完整的 API 与示例(C、C++、Go、Java、Rust,以及一份法语翻译)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index aec6eb6a..eb9f37b2 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -204,7 +204,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **語法高亮** — 任何語言的關鍵字集與規則 - **介面翻譯** — 新的介面語言 - **執行設定** — 為直譯式(`go run main.go`)與編譯式(`gcc main.c -o main` 後執行)語言提供「Run with…」,透過 PyBreeze 的 `FileRunnerProcess` 執行,並在結束後清掉編譯產物 -- **外掛瀏覽器** — 在 IDE 內瀏覽並安裝遠端儲存庫中的外掛 +- **外掛瀏覽器** — 在 IDE 內瀏覽並安裝遠端儲存庫中的外掛,入口是 **Plugins → Plugin Browser**,還沒裝任何外掛時也在;裝好的外掛在下次啟動時載入 已載入的外掛會出現在它們專屬的 **Plugins**(外掛)選單下,附一個 About 項目和一個執行動作,動作名稱標示它能執行的副檔名。[PLUGIN_GUIDE.md](../PLUGIN_GUIDE.md) 說明 PyBreeze 額外提供的部分,並連到 JEditor 的指南,那裡有完整的 API 與實作範例(C、C++、Go、Java、Rust,以及法文翻譯)。 diff --git a/architecture_explore.md b/architecture_explore.md index 90d2a196..fbaea11d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -203,7 +203,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 5.4 插件選單 -- **`build_plugin_menu.py`** — 讀 `je_editor.plugins.get_all_plugin_metadata()`,每個插件一個子選單(About + 一個 Run 動作,多個副檔名時一併列在標籤裡,動作直接呼叫 `run_current_file_with()`);另有「Plugin Browser」分頁入口。插件是第三方程式:不是 dict 的 metadata 或 run config 略過並記 log,名稱經 `plugin_text()` 轉成文字(`addMenu(None)` 會讓 Qt access violation),每個插件的選單各自建、失敗只少它自己那一項 +- **`build_plugin_menu.py`** — 讀 `je_editor.plugins.get_all_plugin_metadata()`,每個插件一個子選單(About + 一個 Run 動作,多個副檔名時一併列在標籤裡,動作直接呼叫 `run_current_file_with()`);另有「Plugin Browser」分頁入口,沒有任何插件時選單也照建、只有這一項(第一個插件就是從它裝的)。插件是第三方程式:不是 dict 的 metadata 或 run config 略過並記 log,名稱經 `plugin_text()` 轉成文字(`addMenu(None)` 會讓 Qt access violation),每個插件的選單各自建、失敗只少它自己那一項 - **`build_run_with_menu.py`** — 讀 `get_all_plugin_run_configs()`,在 Run 選單下加「Run with…」。`run_config_suffixes()` 把插件登記的副檔名正規化成 `Path.suffix` 的樣子(小寫、一個前導點;JEditor 原樣保存,`.R`、`r` 以前永遠比對不上)。`run_current_file_with()` 先經 `save_current_file_for_run()` 存檔(已有檔名的分頁照 JEditor 自己存檔的方式寫:`write_file_with_encoding()` 用分頁的編碼與行尾,寫成功後才 `mark_ignore_next_file_change()` 與 `mark_saved()`;存檔失敗跳警告、不執行;沒檔名的走 JEditor 的另存新檔),再驗副檔名、交給 `FileRunnerProcess`。Plugins 選單的 Run 動作也走這一條 ### 5.5 安裝選單 @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2594 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2595 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ddbc5544..f4acf712 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -609,3 +609,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `docs/source/Eng/ai_tools.rst`, `docs/source/Zh/ai_tools.rst` - **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and neither built page shows a stray `**` or double backquote. - **Open items**: none (whether a local model server should be allowed is `progress.md` #111). + +## U-20260925-72 · 2026-09-25 · The Plugin Browser is there before any plugin is · #fix #plugins + +- **What**: `set_plugin_menu` returned before building the Plugins menu when no plugin was loaded, so on a fresh install there was no Plugins menu and no Plugin Browser, the tool the README offers for installing plugins. The first plugin had to be copied into `jeditor_plugins/` by hand before the browser could be reached. JEditor's own Plugins menu always has the browser. Found while checking the Sphinx Plugins page against the menus. +- **Change**: the Plugins menu is always built with its Plugin Browser entry; the separator and one entry per plugin follow when plugins are loaded. The three READMEs say where the browser is, that it is there before any plugin is installed, and that an installed plugin loads at the next start (the browser says so as it installs). +- **Files**: `pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py`, `test/test_utils/test_plugin_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` (§5.4 and the test count) +- **Checks**: `test_no_plugins_means_no_menu`, which recorded the old behaviour, became `test_with_no_plugins_the_menu_still_offers_the_plugin_browser`; `test_the_plugin_browser_opens_as_a_tab` triggers the entry. Both failed before the change. `test_plugin_menu.py`, `test_answered_boxes_are_deleted.py` and `test_jeditor_contract.py` pass (87). A menu dump of the IDE started in a folder with no `jeditor_plugins/` ends with Plugins > Plugin Browser. `test/test_utils` collects 2595 tests; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a4ca5de5..0a3a9c25 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-72 | 2026-09-25 | The Plugin Browser is there before any plugin is | #fix #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-71 | 2026-09-25 | The Sphinx AI Tools pages told right | #docs #ai | [2026-09-c](2026-09-c.md) | | U-20260925-70 | 2026-09-25 | CoT Code Review labels its code box as the code | #fix #ai | [2026-09-c](2026-09-c.md) | | U-20260925-69 | 2026-09-25 | The Sphinx Tools pages told right | #docs #diagram | [2026-09-c](2026-09-c.md) | @@ -474,4 +475,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 71 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 72 | diff --git a/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py b/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py index 3a4fefc9..a35ceb84 100644 --- a/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py +++ b/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py @@ -26,11 +26,10 @@ def set_plugin_menu(ui_we_want_to_set: PyBreezeMainWindow) -> None: 有執行設定的插件是一個子選單:About 和一個「Run with」項目,支援多種副檔名時一併列出。 A plugin with a run config gets a submenu: About, and one Run with entry that lists the suffixes when there are several. + 外掛瀏覽器一定在:沒有任何外掛時,第一個外掛就是從它安裝的。 + The Plugin Browser is always there, with no plugin loaded too: it is how + the first one gets installed. """ - metadata_list = get_all_plugin_metadata() - if not metadata_list: - return - ui_we_want_to_set.plugin_menu = ui_we_want_to_set.menu.addMenu( language_wrapper.language_word_dict.get("plugin_menu_label", "Plugins") ) @@ -42,8 +41,10 @@ def set_plugin_menu(ui_we_want_to_set: PyBreezeMainWindow) -> None: ) browse_action.triggered.connect(lambda: _open_plugin_browser(ui_we_want_to_set)) ui_we_want_to_set.plugin_menu.addAction(browse_action) - ui_we_want_to_set.plugin_menu.addSeparator() + metadata_list = get_all_plugin_metadata() + if metadata_list: + ui_we_want_to_set.plugin_menu.addSeparator() for meta in metadata_list: # One plugin with bad metadata costs its own entry, not the IDE's start if not isinstance(meta, dict): diff --git a/test/test_utils/test_plugin_menu.py b/test/test_utils/test_plugin_menu.py index 5b288667..5d43d4cd 100644 --- a/test/test_utils/test_plugin_menu.py +++ b/test/test_utils/test_plugin_menu.py @@ -214,10 +214,23 @@ def test_a_config_without_suffixes_accepts_any_file( class TestThePluginMenu: - def test_no_plugins_means_no_menu(self, window, monkeypatch): + def test_with_no_plugins_the_menu_still_offers_the_plugin_browser(self, window, monkeypatch): + # The browser is how a first plugin gets installed: the menu was left + # out while none was loaded, so it could not be reached until one was + # copied into jeditor_plugins/ by hand. JEditor's own menu always has it. + monkeypatch.setattr(plugin_menu, "get_all_plugin_metadata", lambda: []) + set_plugin_menu(window) + assert labels(window.plugin_menu) == ["Plugin Browser"] + + def test_the_plugin_browser_opens_as_a_tab(self, window, monkeypatch): monkeypatch.setattr(plugin_menu, "get_all_plugin_metadata", lambda: []) + monkeypatch.setattr(plugin_menu, "PluginBrowserWidget", QWidget) set_plugin_menu(window) - assert not hasattr(window, "plugin_menu") + + window.plugin_menu.actions()[0].trigger() + + assert window.tab_widget.count() == 1 + assert window.tab_widget.tabText(0).startswith("Plugin Browser") def test_a_plugin_without_a_run_config_gets_a_bare_entry( self, window, monkeypatch): From 33fa0f0d5f29af6bdc815af64abd405fa1bfe5d5 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 12:39:29 +0800 Subject: [PATCH 124/166] Correct the Sphinx Plugins and extension pages --- PLUGIN_GUIDE.md | 4 + README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/source/Eng/how_to_extend_ui.rst | 43 ++++++++- docs/source/Eng/menu_plugins.rst | 137 +++++++++++++++++++-------- docs/source/Zh/how_to_extend_ui.rst | 38 +++++++- docs/source/Zh/menu_plugins.rst | 126 ++++++++++++++++-------- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- progress.md | 1 + 11 files changed, 279 insertions(+), 87 deletions(-) diff --git a/PLUGIN_GUIDE.md b/PLUGIN_GUIDE.md index dded46ab..41e62f65 100644 --- a/PLUGIN_GUIDE.md +++ b/PLUGIN_GUIDE.md @@ -9,6 +9,9 @@ for both editors, kept in the JEditor repository: Ready-made plugins (C, C++, Go, Java and Rust highlighting with run support, a French UI translation) live in [IDE_Plugins](https://github.com/Jeffrey-Plugin-Repos/IDE_Plugins). +JEditor now colours those five languages itself, and a registered language's keywords are used +only for a suffix it does not colour, so from these plugins their run configurations are what +takes effect. PyBreeze reads one run-config key JEditor does not: `"encoding"`, the encoding the program writes its output in (`"cp950"`, `"utf-8"`, or `"locale"` for the machine's own). Without it the output is @@ -25,6 +28,7 @@ PyBreeze 建立在 JEditor 之上,原封不動地使用它的外掛系統:`j 現成的外掛(C、C++、Go、Java、Rust 語法高亮與執行設定,以及法文介面翻譯)放在 [IDE_Plugins](https://github.com/Jeffrey-Plugin-Repos/IDE_Plugins)。 +JEditor 現在自己就會為這五種語言上色,而註冊的語言關鍵字只用在它不上色的副檔名,所以這些外掛實際生效的是執行設定。 PyBreeze 多讀一個 JEditor 不讀的執行設定欄位:`"encoding"`,程式輸出用的編碼(`"cp950"`、`"utf-8"`, 或 `"locale"` 表示這台電腦自己的)。沒寫就用 IDE 的編碼讀,預設 UTF-8。Java 與中文化的編譯器輸出的是 diff --git a/README.md b/README.md index e1306cd0..1bba4f16 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ Define reusable skill prompts (code explanation, code review), then pick one, ed PyBreeze inherits JEditor's plugin architecture, auto-discovered from a `jeditor_plugins/` directory in the working directory. A plugin can register: -- **Syntax highlighting** — keyword sets and rules for any language +- **Syntax highlighting** — keyword sets and rules for a file suffix JEditor does not colour itself (for `.c`, `.cpp`, `.go`, `.java`, `.js`, `.json`, `.rs`, `.sh`, `.sql`, `.toml`, `.ts`, `.yaml` and the other suffixes it colours, its own rules are used) - **UI translations** — new interface languages - **Run configurations** — "Run with…" for interpreted (`go run main.go`) and compiled (`gcc main.c -o main` then run) languages, executed through PyBreeze's `FileRunnerProcess` with the compiled artifact cleaned up afterwards - **Plugin Browser** — browse and install plugins from remote repositories inside the IDE, from **Plugins → Plugin Browser**, which is there before any plugin is installed; an installed plugin loads at the next start diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 3e15e654..47fe1ca7 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -201,7 +201,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_plugins/` 目录自动发现插件。插件可以注册: -- **语法高亮** — 任何语言的关键字集与规则 +- **语法高亮** — JEditor 自身不着色的文件扩展名的关键字集与规则(`.c`、`.cpp`、`.go`、`.java`、`.js`、`.json`、`.rs`、`.sh`、`.sql`、`.toml`、`.ts`、`.yaml` 等 JEditor 会着色的扩展名,使用的是它自己的规则) - **界面翻译** — 新的界面语言 - **运行配置** — 为解释型(`go run main.go`)与编译型(`gcc main.c -o main` 后再运行)语言提供"Run with…",通过 PyBreeze 的 `FileRunnerProcess` 执行,编译产物在运行后会被清理 - **插件浏览器** — 在 IDE 内从远程仓库浏览并安装插件,入口是 **Plugins → Plugin Browser**,尚未安装任何插件时也在;装好的插件在下次启动时加载 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index eb9f37b2..8ab07112 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -201,7 +201,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jeditor_plugins/` 目錄探索外掛。外掛可以註冊: -- **語法高亮** — 任何語言的關鍵字集與規則 +- **語法高亮** — JEditor 自己不上色的副檔名的關鍵字集與規則(`.c`、`.cpp`、`.go`、`.java`、`.js`、`.json`、`.rs`、`.sh`、`.sql`、`.toml`、`.ts`、`.yaml` 等 JEditor 會上色的副檔名,用的是它自己的規則) - **介面翻譯** — 新的介面語言 - **執行設定** — 為直譯式(`go run main.go`)與編譯式(`gcc main.c -o main` 後執行)語言提供「Run with…」,透過 PyBreeze 的 `FileRunnerProcess` 執行,並在結束後清掉編譯產物 - **外掛瀏覽器** — 在 IDE 內瀏覽並安裝遠端儲存庫中的外掛,入口是 **Plugins → Plugin Browser**,還沒裝任何外掛時也在;裝好的外掛在下次啟動時載入 diff --git a/docs/source/Eng/how_to_extend_ui.rst b/docs/source/Eng/how_to_extend_ui.rst index faeb0fdf..4966f671 100644 --- a/docs/source/Eng/how_to_extend_ui.rst +++ b/docs/source/Eng/how_to_extend_ui.rst @@ -51,11 +51,44 @@ How It Works You must register your custom tabs in ``EDITOR_EXTEND_TAB`` **before** calling ``start_editor()``, as the tabs are loaded during window initialization. +A widget whose constructor raises costs only its own tab: the error is logged and the IDE +starts without it. + +Asking Before Closing +--------------------- + +A tab that can hold unsaved work can say so. Give the widget a ``may_close()`` method +returning ``True`` when it may close: closing its tab, or the IDE, asks it first, and +``False`` keeps it open. Ask the user there, as PyBreeze's own prompt and diagram editors +do: + +.. code-block:: python + + from PySide6.QtWidgets import QMessageBox, QTextEdit, QVBoxLayout, QWidget + + + class NotesTab(QWidget): + def __init__(self): + super().__init__() + self.text = QTextEdit() + QVBoxLayout(self).addWidget(self.text) + + def may_close(self) -> bool: + if not self.text.document().isModified(): + return True + reply = QMessageBox.question(self, "Unsaved notes", "Close and lose the notes?") + return reply == QMessageBox.StandardButton.Yes + +A ``may_close()`` that raises counts as a yes (it is logged): it cannot keep the IDE from +closing. + Advanced: Plugin-Based Tabs ---------------------------- You can also add custom tabs via the plugin system by placing a plugin file -in the ``jeditor_plugins/`` directory: +in the ``jeditor_plugins/`` directory (see :doc:`menu_plugins`). Register the tab in the +plugin's ``register()``: plugins load while the main window is being built, before its +tabs are added. .. code-block:: python @@ -63,6 +96,8 @@ in the ``jeditor_plugins/`` directory: from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit from pybreeze import EDITOR_EXTEND_TAB + PLUGIN_NAME = "My Tool" + class MyToolWidget(QWidget): def __init__(self): @@ -73,6 +108,8 @@ in the ``jeditor_plugins/`` directory: layout.addWidget(self.text_edit) - EDITOR_EXTEND_TAB.update({"My Tool": MyToolWidget}) + def register() -> None: + EDITOR_EXTEND_TAB.update({"My Tool": MyToolWidget}) -This plugin will be auto-discovered and loaded when PyBreeze starts. +This plugin will be auto-discovered and loaded when PyBreeze starts from a folder that +has it in ``jeditor_plugins/``. diff --git a/docs/source/Eng/menu_plugins.rst b/docs/source/Eng/menu_plugins.rst index f08a7445..031503cf 100644 --- a/docs/source/Eng/menu_plugins.rst +++ b/docs/source/Eng/menu_plugins.rst @@ -1,78 +1,137 @@ Plugins Menu ============ -PyBreeze supports a plugin system for extending functionality. Plugins are -auto-discovered from the ``jeditor_plugins/`` directory in your working directory. +PyBreeze uses JEditor's plugin system. Plugins are loaded at start-up from the +``jeditor_plugins/`` folder in the working directory (and, for a development checkout of +JEditor, the one beside its package): + +- a ``.py`` file is a plugin, and so is a folder with an ``__init__.py``; +- a folder without ``__init__.py`` only groups plugins, and is searched inside; +- names that start with ``_`` or ``.`` are skipped. + +Each plugin defines a ``register()`` function, called once as it loads. It may also set +``PLUGIN_NAME``, ``PLUGIN_AUTHOR``, ``PLUGIN_VERSION`` and ``PLUGIN_RUN_CONFIG``. Plugin Browser -------------- -Opens the plugin browser interface where you can: +**Plugins > Plugin Browser** opens a tab that lists the plugins in a GitHub repository +(``https://github.com/Jeffrey-Plugin-Repos/IDE_Plugins`` by default; any +``https://github.com/owner/repo`` or ``owner/repo`` can be entered in **Repository URL** +and read with **Fetch Plugins**). Select a plugin to see its details and source, then +**Download & Install** saves it into ``jeditor_plugins/`` in the working directory, asking +before it replaces a plugin of the same name. An installed plugin loads at the next start. -- Browse available plugins -- View plugin details -- Install new plugins +The entry is there before any plugin is installed. Loaded Plugins -------------- -After startup, any plugins found in ``jeditor_plugins/`` are automatically loaded -and listed under the Plugins menu. Each loaded plugin appears as a menu entry. +Below the Plugin Browser, each loaded plugin has an entry: -Run With Menu -------------- +- a plugin without a run configuration (a translation, a syntax plugin): its name, which + shows its name, version and author; +- a plugin with a run configuration: a submenu named after the configuration, with + **About** and **Run with** ** (the suffixes it runs are listed in the label when + there are several). -The **Run With** menu provides options to run the current file using different -compilers and interpreters. This is dynamically built based on available -language support. +Run with... Menu +---------------- -Supported languages include: +The **Run** menu has a **Run with...** submenu once a plugin has registered a run +configuration: one entry per configuration, labelled with its name and suffixes. It runs +the file in the editor tab in front: -- **C** -- Compile and run with gcc/clang -- **C++** -- Compile and run with g++/clang++ -- **Go** -- Run with go run -- **Java** -- Compile and run with javac/java -- **Rust** -- Compile and run with rustc +1. The tab is saved first, in its own encoding and line ending; a tab without a file goes + through **Save As**. A save that fails is reported and nothing runs. +2. A file whose suffix the configuration does not list is refused. +3. The file runs in a run window titled with the configuration's name and the file, with a + **Stop** button: ``compiler [args...] file``, or, for a compiled language, the compiler + first (limited to 60 seconds) and then the program it built. The build goes into a + temporary folder that is removed afterwards. .. note:: - The available "Run With" options depend on which compilers/interpreters are - installed on your system and discoverable via the system PATH. + A run configuration only names the compiler or interpreter; it has to be installed and + on ``PATH``. Otherwise the run window says the command was not found. + +A run configuration is a dictionary: + +.. code-block:: python + + PLUGIN_RUN_CONFIG = { + "name": "Go", # the menu label + "suffixes": (".go",), # the files it runs + "compiler": "go", # the program started + "args": ("run",), # arguments between the compiler and the file + # For a compiled language: + # "compile_then_run": True, "output_flag": "-o", + # PyBreeze only: the encoding the program writes ("locale" is the machine's own) + # "encoding": "locale", + } Creating Plugins ---------------- -For detailed information on creating custom plugins, including syntax highlighting -plugins and UI translation plugins, see the -`Plugin Guide `_ -(the guide lives in the JEditor repository, since PyBreeze uses JEditor's plugin system). +The full plugin API, with worked examples, is JEditor's +`Plugin Guide `_; +PyBreeze's `PLUGIN_GUIDE.md `_ +covers what PyBreeze adds. Ready-made plugins are in +`IDE_Plugins `_. Syntax Highlighting Plugin Example ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Plugins can extend syntax highlighting with custom keywords: +A plugin can colour the keywords of a language, by file suffix: .. code-block:: python - # jeditor_plugins/my_syntax_plugin.py - from je_editor import syntax_word_dict + # jeditor_plugins/lua_syntax.py + from PySide6.QtGui import QColor + from je_editor.plugins import register_programming_language + + PLUGIN_NAME = "Lua syntax" + PLUGIN_VERSION = "1.0" - syntax_word_dict.update({ - "my_keyword": "keyword_format", - "my_function": "function_format", - }) + + def register() -> None: + register_programming_language( + suffix=".lua", + syntax_words={ + "keywords": {"words": ("function", "local", "end", "return"), + "color": QColor(86, 156, 214)}, + }, + syntax_rules={ + "comments": {"rules": (r"--[^\n]*",), "color": QColor(106, 153, 85)}, + }, + ) + +.. note:: + + Registered keywords are used only for a suffix JEditor does not colour itself. For + ``.c``, ``.cpp``, ``.go``, ``.h``, ``.hpp``, ``.java``, ``.js``, ``.json``, ``.rs``, + ``.sh``, ``.sql``, ``.toml``, ``.ts``, ``.yaml`` and ``.yml`` JEditor's own rules apply, + and a plugin's keywords for them are not shown. UI Translation Plugin Example ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Plugins can add new UI translations: +A plugin can add an interface language, listed in the **Language** menu: .. code-block:: python - # jeditor_plugins/my_language_plugin.py - from je_editor import language_wrapper + # jeditor_plugins/french.py + from je_editor.plugins import register_natural_language + + PLUGIN_NAME = "French" + + + def register() -> None: + register_natural_language("French", "Français", { + "file_menu_label": "Fichier", + # ... the translated strings + }) - language_wrapper.language_word_dict.update({ - "application_name": "My Custom Name", - # ... more translations - }) +The keys are those of JEditor's English dictionary and of PyBreeze's +(``pybreeze/extend_multi_language/extend_english.py``). A key the plugin leaves out is +shown in English. diff --git a/docs/source/Zh/how_to_extend_ui.rst b/docs/source/Zh/how_to_extend_ui.rst index 0f1796ba..4a044128 100644 --- a/docs/source/Zh/how_to_extend_ui.rst +++ b/docs/source/Zh/how_to_extend_ui.rst @@ -51,10 +51,39 @@ PyBreeze 支援使用 ``EDITOR_EXTEND_TAB`` 字典擴充 UI 自訂分頁。 您必須在呼叫 ``start_editor()`` **之前** 在 ``EDITOR_EXTEND_TAB`` 中註冊自訂分頁, 因為分頁在視窗初始化時載入。 +建構子拋出例外的元件只會少掉它自己的分頁:錯誤會記入日誌,IDE 照常啟動。 + +關閉前先詢問 +------------ + +可能有未儲存內容的分頁可以表明這一點。為元件加上 ``may_close()`` 方法,可以關閉時回傳 ``True``: +關閉它的分頁或 IDE 時會先問它,回傳 ``False`` 就保持開啟。在這裡詢問使用者,就像 PyBreeze +自己的提示詞編輯器與架構圖編輯器一樣: + +.. code-block:: python + + from PySide6.QtWidgets import QMessageBox, QTextEdit, QVBoxLayout, QWidget + + + class NotesTab(QWidget): + def __init__(self): + super().__init__() + self.text = QTextEdit() + QVBoxLayout(self).addWidget(self.text) + + def may_close(self) -> bool: + if not self.text.document().isModified(): + return True + reply = QMessageBox.question(self, "筆記未儲存", "要關閉並捨棄筆記嗎?") + return reply == QMessageBox.StandardButton.Yes + +拋出例外的 ``may_close()`` 視為同意(會記入日誌):它不能讓 IDE 關不掉。 + 進階:基於外掛的分頁 --------------------- -您也可以透過外掛系統新增自訂分頁,將外掛檔案放在 ``jeditor_plugins/`` 目錄中: +您也可以透過外掛系統新增自訂分頁,將外掛檔案放在 ``jeditor_plugins/`` 目錄中(見 :doc:`menu_plugins`)。 +在外掛的 ``register()`` 裡註冊分頁:外掛在主視窗建立的過程中載入,早於分頁加入。 .. code-block:: python @@ -62,6 +91,8 @@ PyBreeze 支援使用 ``EDITOR_EXTEND_TAB`` 字典擴充 UI 自訂分頁。 from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit from pybreeze import EDITOR_EXTEND_TAB + PLUGIN_NAME = "我的工具" + class MyToolWidget(QWidget): def __init__(self): @@ -72,6 +103,7 @@ PyBreeze 支援使用 ``EDITOR_EXTEND_TAB`` 字典擴充 UI 自訂分頁。 layout.addWidget(self.text_edit) - EDITOR_EXTEND_TAB.update({"我的工具": MyToolWidget}) + def register() -> None: + EDITOR_EXTEND_TAB.update({"我的工具": MyToolWidget}) -此外掛會在 PyBreeze 啟動時自動探索並載入。 +PyBreeze 從 ``jeditor_plugins/`` 中有這個外掛的資料夾啟動時,會自動探索並載入它。 diff --git a/docs/source/Zh/menu_plugins.rst b/docs/source/Zh/menu_plugins.rst index cd3aa0ea..2caee6de 100644 --- a/docs/source/Zh/menu_plugins.rst +++ b/docs/source/Zh/menu_plugins.rst @@ -1,76 +1,126 @@ Plugins 選單 ============ -PyBreeze 支援外掛系統以擴充功能。外掛會自動從工作目錄中的 -``jeditor_plugins/`` 目錄探索載入。 +PyBreeze 使用 JEditor 的外掛系統。外掛在啟動時從工作目錄中的 ``jeditor_plugins/`` 資料夾載入 +(若是 JEditor 的開發用 checkout,也會讀它套件旁的那一個): + +- ``.py`` 檔是一個外掛,含 ``__init__.py`` 的資料夾也是; +- 沒有 ``__init__.py`` 的資料夾只用來分組,會往裡面找; +- 名稱以 ``_`` 或 ``.`` 開頭的會略過。 + +每個外掛都定義一個 ``register()`` 函式,載入時呼叫一次。也可以設定 ``PLUGIN_NAME``、``PLUGIN_AUTHOR``、 +``PLUGIN_VERSION`` 與 ``PLUGIN_RUN_CONFIG``。 外掛瀏覽器 ---------- -開啟外掛瀏覽器介面,您可以: +**Plugins > Plugin Browser** 會開一個分頁,列出 GitHub 儲存庫中的外掛(預設是 +``https://github.com/Jeffrey-Plugin-Repos/IDE_Plugins``;也可以在 **Repository URL** 輸入任何 +``https://github.com/owner/repo`` 或 ``owner/repo``,再按 **Fetch Plugins** 讀取)。選一個外掛可看它的 +詳細資訊與原始碼,按 **Download & Install** 會把它存進工作目錄的 ``jeditor_plugins/``,已有同名外掛時會先詢問是否取代。 +裝好的外掛在下次啟動時載入。 -- 瀏覽可用的外掛 -- 查看外掛詳細資訊 -- 安裝新外掛 +還沒裝任何外掛時,這個項目也在。 已載入外掛 ---------- -啟動後,在 ``jeditor_plugins/`` 中找到的任何外掛都會自動載入, -並列在 Plugins 選單下。每個已載入的外掛都會顯示為一個選單項目。 +在 Plugin Browser 下方,每個已載入的外掛各有一個項目: -Run With 選單 -------------- +- 沒有執行設定的外掛(翻譯、語法外掛):它的名稱,點了會顯示名稱、版本與作者; +- 有執行設定的外掛:以執行設定命名的子選單,內有 **About** 與 **Run with** *<名稱>*\ (能執行多種副檔名時, + 標籤會一併列出)。 -**Run With** 選單提供使用不同編譯器和直譯器執行目前檔案的選項。 -此選單根據可用的語言支援動態建立。 +Run with... 選單 +---------------- -支援的語言包括: +只要有外掛註冊了執行設定,**Run** 選單就會出現 **Run with...** 子選單:每個執行設定一項,標籤是它的名稱與副檔名。 +它執行前景編輯分頁中的檔案: -- **C** -- 使用 gcc/clang 編譯並執行 -- **C++** -- 使用 g++/clang++ 編譯並執行 -- **Go** -- 使用 go run 執行 -- **Java** -- 使用 javac/java 編譯並執行 -- **Rust** -- 使用 rustc 編譯並執行 +1. 先存檔,用分頁自己的編碼與行尾;還沒有檔案的分頁會走 **Save As**。存檔失敗會告知,而且不執行。 +2. 副檔名不在執行設定清單中的檔案會被拒絕。 +3. 檔案在執行視窗中執行,標題是執行設定的名稱與檔名,並有 **Stop** 按鈕:``compiler [args...] file``; + 編譯式語言則先編譯(限時 60 秒),再執行編出的程式。編譯產物放在暫存資料夾,執行後刪除。 .. note:: - 可用的「Run With」選項取決於系統上安裝了哪些編譯器/直譯器, - 以及它們是否可透過系統 PATH 找到。 + 執行設定只指定編譯器或直譯器的名稱,它必須已經安裝並在 ``PATH`` 上,否則執行視窗會說找不到指令。 + +執行設定是一個字典: + +.. code-block:: python + + PLUGIN_RUN_CONFIG = { + "name": "Go", # 選單標籤 + "suffixes": (".go",), # 它執行的檔案 + "compiler": "go", # 啟動的程式 + "args": ("run",), # 放在編譯器與檔案之間的參數 + # 編譯式語言: + # "compile_then_run": True, "output_flag": "-o", + # 只有 PyBreeze 讀:程式輸出的編碼("locale" 表示這台電腦自己的) + # "encoding": "locale", + } 建立外掛 -------- -關於建立自訂外掛的詳細資訊,包括語法高亮外掛和 UI 翻譯外掛, -請參閱 `外掛指南 `_ -(指南放在 JEditor repo,因為 PyBreeze 用的是 JEditor 的外掛系統)。 +完整的外掛 API 與實作範例在 JEditor 的 +`Plugin Guide `_; +PyBreeze 的 `PLUGIN_GUIDE.md `_ +說明 PyBreeze 額外提供的部分。現成的外掛放在 +`IDE_Plugins `_。 語法高亮外掛範例 ^^^^^^^^^^^^^^^^ -外掛可以使用自訂關鍵字擴充語法高亮: +外掛可以依副檔名為一種語言的關鍵字上色: .. code-block:: python - # jeditor_plugins/my_syntax_plugin.py - from je_editor import syntax_word_dict + # jeditor_plugins/lua_syntax.py + from PySide6.QtGui import QColor + from je_editor.plugins import register_programming_language + + PLUGIN_NAME = "Lua syntax" + PLUGIN_VERSION = "1.0" - syntax_word_dict.update({ - "my_keyword": "keyword_format", - "my_function": "function_format", - }) -UI 翻譯外掛範例 + def register() -> None: + register_programming_language( + suffix=".lua", + syntax_words={ + "keywords": {"words": ("function", "local", "end", "return"), + "color": QColor(86, 156, 214)}, + }, + syntax_rules={ + "comments": {"rules": (r"--[^\n]*",), "color": QColor(106, 153, 85)}, + }, + ) + +.. note:: + + 註冊的關鍵字只用在 JEditor 自己不上色的副檔名。``.c``、``.cpp``、``.go``、``.h``、``.hpp``、``.java``、 + ``.js``、``.json``、``.rs``、``.sh``、``.sql``、``.toml``、``.ts``、``.yaml`` 與 ``.yml`` + 用的是 JEditor 自己的規則,外掛為它們註冊的關鍵字不會顯示。 + +介面翻譯外掛範例 ^^^^^^^^^^^^^^^^ -外掛可以新增新的 UI 翻譯: +外掛可以新增一種介面語言,列在 **Language** 選單中: .. code-block:: python - # jeditor_plugins/my_language_plugin.py - from je_editor import language_wrapper + # jeditor_plugins/french.py + from je_editor.plugins import register_natural_language + + PLUGIN_NAME = "French" + + + def register() -> None: + register_natural_language("French", "Français", { + "file_menu_label": "Fichier", + # ... 其他翻譯字串 + }) - language_wrapper.language_word_dict.update({ - "application_name": "我的自訂名稱", - # ... 更多翻譯 - }) +鍵與 JEditor 的英文字典和 PyBreeze 的(``pybreeze/extend_multi_language/extend_english.py``)相同。 +外掛沒有提供的鍵會以英文顯示。 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index f4acf712..7416aa4c 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -617,3 +617,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py`, `test/test_utils/test_plugin_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` (§5.4 and the test count) - **Checks**: `test_no_plugins_means_no_menu`, which recorded the old behaviour, became `test_with_no_plugins_the_menu_still_offers_the_plugin_browser`; `test_the_plugin_browser_opens_as_a_tab` triggers the entry. Both failed before the change. `test_plugin_menu.py`, `test_answered_boxes_are_deleted.py` and `test_jeditor_contract.py` pass (87). A menu dump of the IDE started in a folder with no `jeditor_plugins/` ends with Plugins > Plugin Browser. `test/test_utils` collects 2595 tests; `ruff` clean. - **Open items**: none. + +## U-20260925-73 · 2026-09-25 · The Sphinx Plugins and extension pages told right · #docs #plugins + +- **What**: the audit found the Plugins Menu page, in both languages, with a Run With list of five languages it claimed are supported (the Run with... submenu lists whatever run configurations plugins registered, and exists only when one did), a syntax example that imports `syntax_word_dict` from `je_editor` (there is no such name) with no `register()` (JEditor warns and counts such a plugin as skipped), and a translation example that overwrote the current dictionary instead of registering a language. How to Extend the UI registered its plugin tab at import rather than in `register()`, and said nothing about `may_close()`. Checking them against JEditor 1.0.27 also showed that a registered language's keywords are used only for a suffix JEditor does not colour itself: the README called syntax plugins good "for any language", and PLUGIN_GUIDE offered C, C++, Go, Java and Rust highlighting plugins whose highlighting JEditor's own rules now replace (the cause of `progress.md` #103). +- **Change**: the Plugins page, in both languages, says how plugins are found and loaded, what the Plugin Browser does (repository, install folder, asking before replacing, loaded at the next start), what each loaded plugin's entry holds, how Run with... saves, checks and runs a file, what a run configuration holds (with PyBreeze's `encoding`), and gives a syntax example for `.lua` and a translation example through `register_natural_language`, both inside `register()`, with the suffixes JEditor colours itself. How to Extend the UI registers the plugin tab in `register()` and describes `may_close()`. The three READMEs and PLUGIN_GUIDE (both languages) state the highlighting limit. `progress.md` #113 records that a translation plugin's display name is not used by JEditor's Language menu. +- **Files**: `docs/source/Eng/menu_plugins.rst`, `docs/source/Zh/menu_plugins.rst`, `docs/source/Eng/how_to_extend_ui.rst`, `docs/source/Zh/how_to_extend_ui.rst`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `PLUGIN_GUIDE.md`, `progress.md` +- **Checks**: the three plugin examples, copied from the built page into a `jeditor_plugins/` folder, load in the IDE: the Plugins menu lists French and Lua syntax, the Language menu lists French, the tab example adds a My Tool tab, and the Lua example colours `local`, `function`, `return` and `end` and the comment. `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and no built page shows a stray `**` or double backquote. +- **Open items**: `progress.md` #113. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0a3a9c25..67001b70 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-73 | 2026-09-25 | The Sphinx Plugins and extension pages told right | #docs #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-72 | 2026-09-25 | The Plugin Browser is there before any plugin is | #fix #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-71 | 2026-09-25 | The Sphinx AI Tools pages told right | #docs #ai | [2026-09-c](2026-09-c.md) | | U-20260925-70 | 2026-09-25 | CoT Code Review labels its code box as the code | #fix #ai | [2026-09-c](2026-09-c.md) | @@ -475,4 +476,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 72 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 73 | diff --git a/progress.md b/progress.md index 55985060..18ea27dd 100644 --- a/progress.md +++ b/progress.md @@ -21,3 +21,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#110** [BLOCKED] A file opened from the project tree shows as unsaved (" *" on its tab, `_is_modified` true) before anything is typed: JEditor's `EditorWidget.open_an_file` fills the editor with `setPlainText`, which fires `_on_text_changed`, and never clears the mark afterwards (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:295` and `:347-358` in `D:\Codes\JEDITOR` at 1bf81e3; plain `EditorMain` does the same). Needs `open_an_file` to clear the mark (`mark_saved()`) once the file is loaded. - **#111** [DECIDE] AI Code Review, CoT Code Review and Skill Send refuse an endpoint on this machine or on a private network (`validate_url` in `ai_code_review_gui.py:77`, `code_review_thread.py:33`, `skills_send_gui.py:55`; `url_validation._is_blocked_ip`), so a local model server (Ollama, LM Studio, a prthinker server on localhost) cannot be used from them, although CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` until U-20260925-11. The Security › Network rule rejects loopback and private addresses for every user-supplied URL. Should these panels let the user allow a loopback (or private) endpoint, for example after a confirmation naming the address, or stay public-only? - **#112** [DECIDE] Two of the executable build configs still build the IDE under its old name: `exe/auto_py_to_exe_setting_linux.json` (name AutomationIDE, script `exe/start_automation_editor.py` and icon `exe/je_driver_icon.ico`, neither of which exists any more, all under `C:/CodeWorkspace/Python/AutomationIDE`) and the InstallForge project `exe/automation_ide_setup_config.ifp` (program AutomationIDE 1.0.9, the AutomationIDE repository as its website and licence link, the same icon, `...\AutomationIDE\output\AutomationIDE.exe`). The Windows `exe/auto_py_to_exe_setting.json` builds PyBreeze from `exe/start_pybreeze.py` with `exe/pybreeze_icon.ico` (987252e). Should the two be brought over to PyBreeze the same way (their paths are a build machine's), or deleted if they are no longer used? +- **#113** [BLOCKED] A translation plugin's language is listed in the Language menu under its key, not the name it registers: `register_natural_language(key, display_name, word_dict)` (`je_editor/plugins/__init__.py`) writes the dictionary into `language_wrapper.choose_language_dict` but not `display_names`, so `available_languages()` lists it and `display_name()` gives the key (`je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py:47-49`), and the loop over `get_all_natural_languages()` below skips it as already listed. Seen with the Sphinx Plugins page's French example ("French" shown, not "Français") on je_editor 1.0.27. Needs JEditor's `register_natural_language` to call `language_wrapper.register_language(key, word_dict, display_name)`. From 979a536f2e43391e7ca8ec72273f9d288d2493e7 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 12:50:47 +0800 Subject: [PATCH 125/166] Open the main window from a function of its own --- architecture.md | 2 +- architecture_explore.md | 4 ++-- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- pybreeze/pybreeze_ui/editor_main/main_ui.py | 20 +++++++++++++++++--- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/architecture.md b/architecture.md index a531e29c..d02f02df 100644 --- a/architecture.md +++ b/architecture.md @@ -72,7 +72,7 @@ The layers are presentation (`pybreeze_ui/`), then execution (`extend/`), then f **Startup** ``` -python -m pybreeze → start_editor() → QApplication → PyBreezeMainWindow() +python -m pybreeze → start_editor() → QApplication → open_main_window() → PyBreezeMainWindow() → update_language_dict() [before JEditor picks the startup language] → EditorMain.__init__(extend=True) [JEditor builds the editor, loads jeditor_plugins/] → drop JEditor Help menu → add_menu_to_menubar() diff --git a/architecture_explore.md b/architecture_explore.md index fbaea11d..71b18368 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -62,11 +62,11 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ## 3. 啟動流程 -`pybreeze/pybreeze_ui/editor_main/main_ui.py:189` 的 `start_editor()`: +`pybreeze/pybreeze_ui/editor_main/main_ui.py:191` 的 `start_editor()`(第 2 步到 `startup_setting()` 在 `open_main_window()`,它回傳視窗給 `start_editor()` 握到程式結束): 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 735 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 759 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - `show_only_warnings_in_code_result()`(`pybreeze_ui/code_result_logs.py`)— JEditor 剛把一個 `RedirectStdErr` 掛到當下每個 logger 上、收到的顯示在 Code Result;自動化套件 import 時把 root 設成 DEBUG,所以開檔就有 gitpython 的除錯訊息以紅字出現。把這個 handler 的門檻調到 WARNING,logger 本身的層級不動 - 刪掉 JEditor 原本的 Help 選單 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 7416aa4c..afd8aa2b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -625,3 +625,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `docs/source/Eng/menu_plugins.rst`, `docs/source/Zh/menu_plugins.rst`, `docs/source/Eng/how_to_extend_ui.rst`, `docs/source/Zh/how_to_extend_ui.rst`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `PLUGIN_GUIDE.md`, `progress.md` - **Checks**: the three plugin examples, copied from the built page into a `jeditor_plugins/` folder, load in the IDE: the Plugins menu lists French and Lua syntax, the Language menu lists French, the tab example adds a My Tool tab, and the Lua example colours `local`, `function`, `return` and `end` and the comment. `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and no built page shows a stray `**` or double backquote. - **Open items**: `progress.md` #113. + +## U-20260925-74 · 2026-09-25 · The main window opens from its own function · #refactor #startup + +- **What**: refactor, no change in behaviour. `start_editor()` built the window, styled it, showed it and applied the saved settings, then ran the application and ended in `os._exit()`, so none of those steps could be tested without ending the test run; the theme fix in the next entry needs a test there. +- **Change**: `open_main_window(app, debug_mode, theme, **kwargs)` in `main_ui.py` does those steps and returns the window, which `start_editor()` holds until the application ends. `architecture.md` §4's start-up flow and `architecture_explore.md` §3 name it (and §3's translation count, 735, is now the 759 there are). +- **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `architecture.md`, `architecture_explore.md` +- **Checks**: `test_gui_thread_gc.py`, `test_package_facade.py` and `test_jeditor_contract.py` pass (51); both start-up tests (`start_automation_test`, `extend_automation_test`) exit 0; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 67001b70..8dcb2b25 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-74 | 2026-09-25 | The main window opens from its own function | #refactor #startup | [2026-09-c](2026-09-c.md) | | U-20260925-73 | 2026-09-25 | The Sphinx Plugins and extension pages told right | #docs #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-72 | 2026-09-25 | The Plugin Browser is there before any plugin is | #fix #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-71 | 2026-09-25 | The Sphinx AI Tools pages told right | #docs #ai | [2026-09-c](2026-09-c.md) | @@ -476,4 +477,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 73 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 74 | diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index e009e126..b6f028f3 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -201,8 +201,23 @@ def start_editor(debug_mode: bool = False, theme: str = "dark_amber.xml", **kwar # Workers allocate enough to trigger a collection, which then destroyed # Qt objects on the worker and crashed the IDE later collect_garbage_on_gui_thread(new_ide) + # Held until the application ends: the window is nobody else's + _window = open_main_window(new_ide, debug_mode=debug_mode, theme=theme, **kwargs) + ret = new_ide.exec() + os._exit(ret) + + +def open_main_window(app: QApplication, debug_mode: bool = False, theme: str = "dark_amber.xml", + **kwargs) -> PyBreezeMainWindow: + """Build the main window, style it, show it and apply the saved settings. + + :param app: the running application, which the theme is applied to + :param debug_mode: close by itself after a while, as the startup tests need + :param theme: qt_material theme name + :return: the window, which the caller keeps for as long as the IDE runs + """ window = PyBreezeMainWindow(debug_mode=debug_mode, **kwargs) - apply_stylesheet(new_ide, theme=theme) + apply_stylesheet(app, theme=theme) window.showMaximized() try: window.startup_setting() @@ -210,5 +225,4 @@ def start_editor(debug_mode: bool = False, theme: str = "dark_amber.xml", **kwar # a bad one is logged and the IDE starts without it. except (OSError, ValueError, TypeError, KeyError, RuntimeError) as error: pybreeze_logger.error("Startup setting error: %r", error) - ret = new_ide.exec() - os._exit(ret) + return window From 0a37729f91abcd231d5342de08b8151182dbde88 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 12:59:15 +0800 Subject: [PATCH 126/166] Show the theme start_editor is given --- README.md | 4 +- README/README_zh-CN.md | 4 +- README/README_zh-TW.md | 4 +- architecture.md | 4 +- architecture_explore.md | 4 +- docs/source/Eng/getting_started.rst | 12 +++--- docs/source/Eng/ui_overview.rst | 3 ++ docs/source/Zh/getting_started.rst | 11 +++--- docs/source/Zh/ui_overview.rst | 2 + docs/updates/2026-09-c.md | 8 ++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/editor_main/main_ui.py | 21 +++++++--- test/test_utils/started_window.py | 15 ++++--- test/test_utils/test_jeditor_contract.py | 10 +++++ test/test_utils/test_start_theme.py | 43 +++++++++++++++++++++ 15 files changed, 118 insertions(+), 30 deletions(-) create mode 100644 test/test_utils/test_start_theme.py diff --git a/README.md b/README.md index 1bba4f16..c034c0cd 100644 --- a/README.md +++ b/README.md @@ -325,8 +325,8 @@ python exe/start_pybreeze.py # from the exe directory ```python from pybreeze import start_editor -start_editor() # default dark_amber theme -start_editor(theme="dark_teal.xml") # any qt_material theme +start_editor() # the theme picked from UI Style (dark_amber until one is) +start_editor(theme="dark_teal.xml") # any qt_material theme; it becomes the picked one ``` Once launched: diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 47fe1ca7..3a4a060d 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -325,8 +325,8 @@ python exe/start_pybreeze.py # 从 exe 目录运行 ```python from pybreeze import start_editor -start_editor() # 默认 dark_amber 主题 -start_editor(theme="dark_teal.xml") # 任意 qt_material 主题 +start_editor() # 在 UI Style 中选定的主题(尚未选择时为 dark_amber) +start_editor(theme="dark_teal.xml") # 任意 qt_material 主题;它会成为选定的主题 ``` 启动后: diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 8ab07112..46c4dc23 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -325,8 +325,8 @@ python exe/start_pybreeze.py # 從 exe 目錄 ```python from pybreeze import start_editor -start_editor() # 預設 dark_amber 主題 -start_editor(theme="dark_teal.xml") # 任何 qt_material 主題 +start_editor() # 從 UI Style 選的主題(還沒選過時是 dark_amber) +start_editor(theme="dark_teal.xml") # 任何 qt_material 主題;它會成為選定的主題 ``` 啟動後: diff --git a/architecture.md b/architecture.md index d02f02df..53a4a1fe 100644 --- a/architecture.md +++ b/architecture.md @@ -53,7 +53,8 @@ The layers are presentation (`pybreeze_ui/`), then execution (`extend/`), then f `multiprocessing.freeze_support()`: in the packaged executable the regex tester runs patterns in a spawned process, which re-runs the executable. From source it runs them in a plain worker script (`python -I -S -c`), so a launch script without the guard is safe. -- **Programmatic**: `pybreeze.start_editor(debug_mode=False, theme="dark_amber.xml", **kwargs)`. +- **Programmatic**: `pybreeze.start_editor(debug_mode=False, theme=None, **kwargs)`. A `theme` replaces + the one picked from UI Style (JEditor's saved `ui_style`) and is saved as it; `None` keeps the saved one. `debug_mode=True` adds an auto-close timer, which CI uses. - **Main window**: `PyBreezeMainWindow` exposes `tab_widget`, `current_run_code_window` and `python_compiler`. @@ -148,6 +149,7 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() | `DEFAULT_ENCODING`, `LINE_ENDING_LF` | `utils.encodings.text_codec` | `menu/plugin_menu/build_run_with_menu.py` | | `actually_color_dict` | `pyside_ui.main_ui.save_settings.user_color_setting_file` | `show_code_window/code_window.py`, `automation_menu/auto_control_menu/build_autocontrol_menu.py`, `tools_gui/diff_gui.py` (the diff's line colours: `diff_added_marker_color`, `diff_removed_marker_color`, `syntax_keyword_color`, `blame_annotation_color`) | | `RedirectStdErr` | `utils.redirect_manager.redirect_manager_class` | `code_result_logs.py` (the handler `EditorMain` hooks onto every logger to show records in Code Result; PyBreeze raises its level to `WARNING`) | + | `user_setting_dict` | `pyside_ui.main_ui.save_settings.user_setting_file` | `editor_main/main_ui.py` (`open_main_window()` makes a `theme` given to `start_editor()` the saved `ui_style`, which `EditorMain.startup_setting()` applies over any theme applied before it) | PyBreeze also relies on `EditorWidget`'s `current_file`, `code_edit`, `file_encoding`, `line_ending`, `mark_ignore_next_file_change()` and `mark_saved()`, on its private `_file_watcher`, diff --git a/architecture_explore.md b/architecture_explore.md index 71b18368..ef20d83a 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -77,7 +77,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE - `setup_file_tree_context_menu()` — 掛上檔案樹右鍵選單,以及焦點在樹上時的 F2(重新命名)與 Delete(刪除,先問、預設否)快捷鍵(`_attach_keys()`,`WidgetShortcut`,走選單的同一組動作)。改名時開著的分頁跟著檔案走(改資料夾也一樣,底下每個開著的檔案都跟著走):先停掉分頁的自動存檔、改名、再用新路徑重開一條(`_stop_auto_save()` / `_start_auto_save()`)——JEditor 的存檔執行緒只認開檔當下的路徑,沒辦法改指向。外部修改監視也跟著搬(改名前就先移除,檔案搬走後 Windows 放不掉舊名),並照 `open_an_file` 重載語法高亮、git 基準與語言伺服器(`rename_self_tab()` 會清掉「未儲存」標記,有未存的編輯就用 `_on_text_changed()` 放回去,否則改名後五秒內關分頁會直接丟掉編輯);Dock Editor(`FullEditorWidget`,關閉時才寫回、檔案不存在就不寫)的 `current_file` 也改指新路徑(`_dock_editors_under()`)。新增與改名的名稱不能帶磁碟代號、根目錄、`..` 或 `:`,也不能解析到資料夾外(`_inside()`;改名只能是單一名稱)。刪除先移到系統的回收筒(`_move_to_trash()`:`QFile.moveToTrash`,Windows 的資源回收筒、macOS 與 freedesktop 的垃圾桶);沒有回收筒可用時再問一次(預設否)才永久刪除,資料夾用 `remove_folder()`(唯讀檔清掉唯讀屬性再刪,git 的物件檔就是唯讀);符號連結與 junction 只刪連結本身,不進回收筒。刪除時同樣用 `_editors_under()`:檔案或資料夾底下每個開著的分頁先停掉自動存檔,再刪;刪完只關掉檔案真的不見了的分頁,刪不掉(被鎖住、唯讀)的檔案分頁留著、自動存檔重開。「在檔案總管中顯示」由 `reveal_command()` 組指令:Windows 用 Explorer 的 `/select,`、macOS 用 `open -R` 把檔案選起來,其他平台 `xdg-open` 只能開資料夾;啟動失敗(例如沒有 `xdg-open`)經 `_perform_file_op()` 跳警告 - `close_tab()` 覆寫 JEditor 的:分頁有 `may_close()` 就先問(提示詞編輯器、架構圖編輯器有未存的變更時會問);關掉的工具分頁 `deleteLater()`(JEditor 的 `removeTab` 不刪 widget,關過的工具分頁會留到 IDE 結束),JEditor 自己的編輯器分頁不動,關閉 IDE 時也先問過每個分頁與 dock,有一個說不就取消關閉 - `debug_mode=True` 時啟動 10 秒自動關閉 `QTimer`(CI 用) -3. `apply_stylesheet()` 套 qt_material 主題(預設 `dark_amber.xml`) +3. `apply_stylesheet()` 套 qt_material 主題:JEditor 存下的 `ui_style`(UI Style 選單選的,沒選過是 `dark_amber.xml`)。`start_editor(theme=...)` 給的主題先寫進 `user_setting_dict["ui_style"]`:`startup_setting()` 會把存下的主題蓋在之前套的任何主題上,以前這個參數因此從來看不到 4. `showMaximized()` → `startup_setting()` → `app.exec()` 5. 離開時以 `os._exit(ret)` 硬退出(避開 Qt 拆解殘留執行緒) @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 143 個 `test_*.py`、2595 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 144 個 `test_*.py`、2601 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/source/Eng/getting_started.rst b/docs/source/Eng/getting_started.rst index 2ff0e64f..58466502 100644 --- a/docs/source/Eng/getting_started.rst +++ b/docs/source/Eng/getting_started.rst @@ -39,8 +39,8 @@ Launching PyBreeze from pybreeze import start_editor - # Available themes: dark_amber.xml (default), dark_teal.xml, - # dark_blue.xml, light_blue.xml, etc. + # Any theme the UI Style menu lists: dark_teal.xml, dark_blue.xml, + # light_blue.xml, ... It replaces the theme picked from UI Style. start_editor(theme="dark_teal.xml") Parameters @@ -59,9 +59,11 @@ Parameters - ``False`` - Auto-close after 10 seconds (for CI testing) * - ``theme`` - - str - - ``"dark_amber.xml"`` - - Qt Material theme name + - str or None + - ``None`` + - Qt Material theme name. It replaces the theme picked from **UI Style** and is kept + as the picked one. ``None`` starts with the picked theme (``dark_amber.xml`` until + one is picked). First Launch ------------ diff --git a/docs/source/Eng/ui_overview.rst b/docs/source/Eng/ui_overview.rst index a17d326a..36955b01 100644 --- a/docs/source/Eng/ui_overview.rst +++ b/docs/source/Eng/ui_overview.rst @@ -148,6 +148,9 @@ at launch time: start_editor(theme="dark_teal.xml") +The theme picked from **UI Style** is saved and used at the next start. A theme given to +``start_editor`` replaces it, and is saved in its place. + Multi-Language Support ---------------------- diff --git a/docs/source/Zh/getting_started.rst b/docs/source/Zh/getting_started.rst index 5c03a8c1..26bdf9f0 100644 --- a/docs/source/Zh/getting_started.rst +++ b/docs/source/Zh/getting_started.rst @@ -39,8 +39,8 @@ from pybreeze import start_editor - # 可用主題:dark_amber.xml(預設)、dark_teal.xml、 - # dark_blue.xml、light_blue.xml 等 + # UI Style 選單列出的任何主題:dark_teal.xml、 + # dark_blue.xml、light_blue.xml……它會取代從 UI Style 選的主題。 start_editor(theme="dark_teal.xml") 參數說明 @@ -59,9 +59,10 @@ - ``False`` - 10 秒後自動關閉(用於 CI 測試) * - ``theme`` - - str - - ``"dark_amber.xml"`` - - Qt Material 主題名稱 + - str 或 None + - ``None`` + - Qt Material 主題名稱。它會取代從 **UI Style** 選的主題,並成為選定的主題。``None`` 表示用選定的主題 + (還沒選過時是 ``dark_amber.xml``)。 首次啟動 -------- diff --git a/docs/source/Zh/ui_overview.rst b/docs/source/Zh/ui_overview.rst index af3aa2f6..77fdb1f4 100644 --- a/docs/source/Zh/ui_overview.rst +++ b/docs/source/Zh/ui_overview.rst @@ -143,6 +143,8 @@ PyBreeze 使用 `qt_material `_ 進行 start_editor(theme="dark_teal.xml") +從 **UI Style** 選的主題會被儲存,下次啟動時使用。傳給 ``start_editor`` 的主題會取代它,並改存成這一個。 + 多語言支援 ---------- diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index afd8aa2b..1f15198f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -633,3 +633,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `architecture.md`, `architecture_explore.md` - **Checks**: `test_gui_thread_gc.py`, `test_package_facade.py` and `test_jeditor_contract.py` pass (51); both start-up tests (`start_automation_test`, `extend_automation_test`) exit 0; `ruff` clean. - **Open items**: none. + +## U-20260925-75 · 2026-09-25 · start_editor's theme is the one the IDE shows · #fix #startup + +- **What**: `start_editor(theme="dark_teal.xml")`, as the README, the Sphinx pages and `architecture.md` show it, never changed the theme. `start_editor` applied the argument, then JEditor's `startup_setting()` applied the saved `ui_style` over it: the theme picked from UI Style, and `dark_amber.xml` when none had been, so even a first launch ended in dark amber. Found while checking the Sphinx pages' UI Style section against JEditor 1.0.27. +- **Change**: `open_main_window()` writes a given theme into JEditor's `user_setting_dict["ui_style"]` before the settings are applied, so it is the one shown and is saved as the picked theme, as picking it from UI Style does. `theme` now defaults to `None`, which keeps the picked theme; the first `apply_stylesheet` uses the picked theme too, so a saved light theme no longer shows dark for a moment before it. `user_setting_dict` joins the JEditor internals `test_jeditor_contract.py` pins, with a test that `startup_setting()` still reads `ui_style`, and `architecture.md` §6's table. The three READMEs' quick start, `architecture.md` §3, `architecture_explore.md` §3 and the Sphinx Getting Started and UI Overview pages (both languages) say what the argument does. +- **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_start_theme.py` (new), `test/test_utils/started_window.py` (`saved_settings` and `build`), `test/test_utils/test_jeditor_contract.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`, `architecture_explore.md`, `docs/source/Eng/getting_started.rst`, `docs/source/Zh/getting_started.rst`, `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst` +- **Checks**: `test_start_theme.py` starts the real window in a child and reads the theme qt_material applied: a given theme is shown and saved, and replaces a saved one (both failed before the change, showing the saved theme); with none given, the saved one, or `dark_amber.xml`, is shown. The contract, start-up language, facade and GC tests pass (81); both start-up tests exit 0; the Sphinx build gives no warning. `test/test_utils` collects 2601 tests in 144 files; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8dcb2b25..083e8040 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-75 | 2026-09-25 | start_editor's theme is the one the IDE shows | #fix #startup | [2026-09-c](2026-09-c.md) | | U-20260925-74 | 2026-09-25 | The main window opens from its own function | #refactor #startup | [2026-09-c](2026-09-c.md) | | U-20260925-73 | 2026-09-25 | The Sphinx Plugins and extension pages told right | #docs #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-72 | 2026-09-25 | The Plugin Browser is there before any plugin is | #fix #plugins | [2026-09-c](2026-09-c.md) | @@ -477,4 +478,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 74 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 75 | diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index b6f028f3..1d4d0351 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -12,6 +12,7 @@ from PySide6.QtWidgets import QApplication, QWidget from je_editor import EditorMain, EditorWidget, language_wrapper from je_editor.pyside_ui.main_ui.dock.destroy_dock import DestroyDock +from je_editor.pyside_ui.main_ui.save_settings.user_setting_file import user_setting_dict from qt_material import apply_stylesheet from pybreeze.extend_multi_language.update_language_dict import update_language_dict @@ -29,6 +30,9 @@ EDITOR_EXTEND_TAB: dict[str, type[QWidget]] = { } +# The theme until one is picked from UI Style, as JEditor has it +DEFAULT_THEME = "dark_amber.xml" + def _close_guarded(widget: QWidget, *steps) -> None: """Run *widget*'s closing *steps*, logging a failure instead of raising it. @@ -188,11 +192,13 @@ def debug_close(self) -> None: app.quit() -def start_editor(debug_mode: bool = False, theme: str = "dark_amber.xml", **kwargs) -> None: +def start_editor(debug_mode: bool = False, theme: str | None = None, **kwargs) -> None: """ Start editor instance :param debug_mode: enable debug mode with auto-close timer - :param theme: qt_material theme name (e.g. "dark_amber.xml", "dark_teal.xml", "light_blue.xml") + :param theme: qt_material theme name (e.g. "dark_teal.xml", "light_blue.xml"). It replaces + the theme picked from UI Style, and is kept as the picked one. ``None`` starts with the + picked theme, ``dark_amber.xml`` until one is picked :return: None """ new_ide = QCoreApplication.instance() @@ -207,17 +213,22 @@ def start_editor(debug_mode: bool = False, theme: str = "dark_amber.xml", **kwar os._exit(ret) -def open_main_window(app: QApplication, debug_mode: bool = False, theme: str = "dark_amber.xml", +def open_main_window(app: QApplication, debug_mode: bool = False, theme: str | None = None, **kwargs) -> PyBreezeMainWindow: """Build the main window, style it, show it and apply the saved settings. :param app: the running application, which the theme is applied to :param debug_mode: close by itself after a while, as the startup tests need - :param theme: qt_material theme name + :param theme: qt_material theme name, which replaces the one picked from UI Style and is + kept as the picked one; ``None`` keeps the picked one :return: the window, which the caller keeps for as long as the IDE runs """ window = PyBreezeMainWindow(debug_mode=debug_mode, **kwargs) - apply_stylesheet(app, theme=theme) + # startup_setting() applies the picked theme, over any applied before it: + # a theme given here was never seen until it became the picked one + if theme is not None: + user_setting_dict["ui_style"] = theme + apply_stylesheet(app, theme=user_setting_dict.get("ui_style", DEFAULT_THEME)) window.showMaximized() try: window.startup_setting() diff --git a/test/test_utils/started_window.py b/test/test_utils/started_window.py index 133b9673..aeb6e263 100644 --- a/test/test_utils/started_window.py +++ b/test/test_utils/started_window.py @@ -43,7 +43,8 @@ def run_started_window( tmp_path: Path, body: str, *, saved_language: str | None = None, - before_window: str = "") -> object: + before_window: str = "", saved_settings: dict | None = None, + build: str = _BUILD_WINDOW) -> object: """Start the IDE in a child, run *body* there, and return the ``result`` it set. *body* runs after the window is built and garbage has been collected, with @@ -52,13 +53,17 @@ def run_started_window( where JEditor looks for its settings, so *saved_language* is written there as the saved language before the start. *before_window* runs after the application exists and before the window is built -- to register an - ``EDITOR_EXTEND_TAB``, say. + ``EDITOR_EXTEND_TAB``, say. *saved_settings* are further saved settings + (``{"ui_style": ...}``), and *build* replaces the code that builds + ``window`` -- to start it the way ``start_editor`` does, say. """ + settings = dict(saved_settings or {}) if saved_language is not None: + settings["language"] = saved_language + if settings: settings_dir = tmp_path / ".jeditor" settings_dir.mkdir() - (settings_dir / "user_setting.json").write_text( - json.dumps({"language": saved_language}), encoding="utf-8") + (settings_dir / "user_setting.json").write_text(json.dumps(settings), encoding="utf-8") result_file = tmp_path / "result.json" environment = { **os.environ, @@ -67,7 +72,7 @@ def run_started_window( filter(None, [str(_REPOSITORY_ROOT), os.environ.get("PYTHONPATH")])), } completed = subprocess.run( # noqa: S603 — fixed argv: this interpreter and a script built from literals - [sys.executable, "-c", _PRELUDE + before_window + _BUILD_WINDOW + body + _REPORT, + [sys.executable, "-c", _PRELUDE + before_window + build + body + _REPORT, str(result_file)], cwd=tmp_path, env=environment, capture_output=True, timeout=_START_TIMEOUT_SECONDS, check=False, shell=False, diff --git a/test/test_utils/test_jeditor_contract.py b/test/test_utils/test_jeditor_contract.py index b61e5b3b..6467ef74 100644 --- a/test/test_utils/test_jeditor_contract.py +++ b/test/test_utils/test_jeditor_contract.py @@ -46,6 +46,8 @@ "show_code_window/code_window.py, auto_control_menu/build_autocontrol_menu.py, tools_gui/diff_gui.py"), ("je_editor.utils.redirect_manager.redirect_manager_class", "RedirectStdErr", "code_result_logs.py"), + ("je_editor.pyside_ui.main_ui.save_settings.user_setting_file", "user_setting_dict", + "editor_main/main_ui.py"), ] # Names PyBreeze imports from je_editor's top level, i.e. from its __all__. @@ -86,6 +88,14 @@ def test_the_main_window_takes_the_arguments_pybreeze_passes(self): assert callable(EditorMain.clear_code_result) assert callable(EditorMain.startup_setting) + def test_the_startup_applies_the_saved_theme(self): + from je_editor import EditorMain + + # open_main_window() gives start_editor's theme to startup_setting() as the saved one. + assert '"ui_style"' in inspect.getsource(EditorMain.startup_setting) + settings = _internal("je_editor.pyside_ui.main_ui.save_settings.user_setting_file", "user_setting_dict") + assert settings.get("ui_style") is not None + def test_an_editor_tab_can_be_saved_the_way_a_run_saves_it(self): from je_editor import EditorWidget diff --git a/test/test_utils/test_start_theme.py b/test/test_utils/test_start_theme.py new file mode 100644 index 00000000..f0422ba2 --- /dev/null +++ b/test/test_utils/test_start_theme.py @@ -0,0 +1,43 @@ +"""The theme ``start_editor(theme=...)`` is given is the one the IDE shows. + +JEditor's ``startup_setting()`` applies the saved theme (``dark_amber.xml`` until +one is picked from UI Style) after the window is shown, over the one +``start_editor`` had applied, so the argument never showed at all. +""" +from __future__ import annotations + +from test_utils.started_window import run_started_window + +_OPEN = """ +import os +from pybreeze.pybreeze_ui.editor_main.main_ui import open_main_window +from je_editor.pyside_ui.main_ui.save_settings.user_setting_file import user_setting_dict +window = open_main_window(app, debug_mode=True{theme}) +gc.collect() +""" + +_READ = """ +result = {"shown": os.environ.get("QTMATERIAL_THEME"), "saved": user_setting_dict.get("ui_style")} +""" + + +def _start(tmp_path, theme: str | None = None, saved: str | None = None) -> dict: + return run_started_window( + tmp_path, _READ, build=_OPEN.format(theme="" if theme is None else f", theme={theme!r}"), + saved_settings={"ui_style": saved} if saved else None) + + +def test_a_theme_given_at_launch_is_the_one_shown(tmp_path): + assert _start(tmp_path, theme="dark_teal.xml") == {"shown": "dark_teal.xml", "saved": "dark_teal.xml"} + + +def test_a_theme_given_at_launch_replaces_the_one_picked_before(tmp_path): + assert _start(tmp_path, theme="dark_teal.xml", saved="light_blue.xml")["shown"] == "dark_teal.xml" + + +def test_without_one_the_theme_picked_before_is_shown(tmp_path): + assert _start(tmp_path, saved="light_blue.xml")["shown"] == "light_blue.xml" + + +def test_without_either_the_default_is_shown(tmp_path): + assert _start(tmp_path)["shown"] == "dark_amber.xml" From fd076c6ae9d2c597d336ed5d84271a8be422959f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:07:12 +0800 Subject: [PATCH 127/166] Correct the Sphinx overview, getting-started and base-menu pages --- docs/source/Eng/getting_started.rst | 35 ++++- docs/source/Eng/menu_file_run_text.rst | 175 +++++++++++++++++-------- docs/source/Eng/ui_overview.rst | 152 ++++++++++++--------- docs/source/Zh/getting_started.rst | 30 ++++- docs/source/Zh/menu_file_run_text.rst | 151 ++++++++++++++------- docs/source/Zh/ui_overview.rst | 129 +++++++++--------- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- progress.md | 2 + 9 files changed, 455 insertions(+), 230 deletions(-) diff --git a/docs/source/Eng/getting_started.rst b/docs/source/Eng/getting_started.rst index 58466502..d2a2d432 100644 --- a/docs/source/Eng/getting_started.rst +++ b/docs/source/Eng/getting_started.rst @@ -4,8 +4,9 @@ Getting Started Requirements ------------ -- Python 3.10 or higher +- Python 3.10 to 3.14 - pip (Python package manager) +- Windows, macOS or Linux; PySide6 is installed with PyBreeze Installation ------------ @@ -16,6 +17,19 @@ Install PyBreeze from PyPI: pip install pybreeze +Or from source: + +.. code-block:: bash + + git clone https://github.com/Integration-Automation/PyBreeze.git + cd PyBreeze + pip install -r requirements.txt + +Either way the automation modules (AutoControl, APITestka, WebRunner, LoadDensity, +FileAutomation, MailThunder, TestPioneer), paramiko and JupyterLab are installed with it. +The **Install** menu upgrades them later, into the interpreter runs use (see +:doc:`menu_install`). + Launching PyBreeze ------------------ @@ -57,7 +71,7 @@ Parameters * - ``debug_mode`` - bool - ``False`` - - Auto-close after 10 seconds (for CI testing) + - Close by itself after 10 seconds (for start-up tests) * - ``theme`` - str or None - ``None`` @@ -65,6 +79,20 @@ Parameters as the picked one. ``None`` starts with the picked theme (``dark_amber.xml`` until one is picked). +The Working Folder +------------------ + +Start PyBreeze from your project folder. The folder it starts in, or the one opened +later with **File > Open Folder**, is where: + +- the file tree opens; +- a ``venv/`` or ``.venv/`` is looked for, to run scripts with when no interpreter is + chosen under **Python Env**; +- **Create ... Project** and the TestPioneer template are written. + +Plugins are loaded once, at start-up, from ``jeditor_plugins/`` in the folder PyBreeze +starts in (see :doc:`menu_plugins`). + First Launch ------------ @@ -73,7 +101,8 @@ When PyBreeze starts, the main window opens maximized with: 1. **Menu Bar** at the top with all available menus 2. **File Tree** on the left side for project navigation 3. **Code Editor** (tabbed) in the center for editing files -4. **Output Panel** at the bottom for execution results +4. **Output panel** below the editor, whose **Code result** tab shows the output of **Run Program** and + **Run On Shell** PyBreeze inherits its core editor functionality from **JEditor** and extends it with automation-specific menus, tools, and integrations. diff --git a/docs/source/Eng/menu_file_run_text.rst b/docs/source/Eng/menu_file_run_text.rst index b0c71570..dcfc80fc 100644 --- a/docs/source/Eng/menu_file_run_text.rst +++ b/docs/source/Eng/menu_file_run_text.rst @@ -1,8 +1,12 @@ File, Run, Text & Other Base Menus =================================== -These menus are inherited from the JEditor base editor engine and provide -core editing and execution functionality. +These menus come from the JEditor editor engine PyBreeze is built on. Almost every entry +acts on the editor tab in front, and does nothing when the tab in front is not an editor. + +JEditor keeps these settings in ``.jeditor/user_setting.json`` in the working folder, so +each project folder has its own; they are saved every minute and when the IDE closes. An +editor tab that has a file is also saved to it every few seconds. File Menu --------- @@ -13,13 +17,26 @@ File Menu * - Menu Item - Description + * - **New File** + - Asks for a name and a place, and creates an empty file there (it is not opened). * - **Open File** - - Opens a file dialog to select a file. The file content is loaded into the code editor. + - Opens a file into the editor tab in front, replacing what it shows. + * - **Open Folder** + - Makes a folder the working folder: the file tree shows it, and its settings are + loaded (see :doc:`getting_started`). * - **Save File** - - Saves the current code editor content to the currently opened file. - * - **Encoding** - - Opens a dialog to choose the encoding for the program runner and shell runner - (e.g., UTF-8, ASCII, Big5). + - Opens a **Save As** dialog, starting in the working folder, and writes the tab there. + * - **Recent Files** + - The last files opened; one opens in a new tab. The list is rebuilt at start-up. + * - **Font** / **Font Size** + - The font and size of the whole window: menus, trees and panels. + * - **Encodings** + - The encoding of the tab's file. The file is read again in it when the tab has no + unsaved edits, and the next save writes it. + * - **Line Endings** + - ``LF``, ``CRLF`` or ``CR`` for the next save of the tab's file. + * - **Save All** + - Writes every editor tab that has a file. Run Menu -------- @@ -30,28 +47,40 @@ Run Menu * - Menu Item - Description - * - **Run Program** - - Executes the current code editor content using the program runner (Python interpreter). - * - **Run On Shell** - - Executes the current code editor content using the system shell. + * - **Run Program > Run Program** + - Opens a **Save As** dialog, writes the tab, then runs the file with Python; the + output goes to the tab's **Code result**. One program at a time per tab. + * - **Run Program > Show program input** + - A small window whose line is sent to the running program's standard input. + * - **Run On Shell > Run On Shell** + - Runs the tab's text, as it is, as one shell command (``cmd.exe`` on Windows); the + output goes to **Code result**. + * - **Run On Shell > Show shell input** + - The same input window, for the shell. + * - **Debugger > Run Debugger** + - Opens a **Save As** dialog, then runs the file under ``pdb`` with the breakpoints set + in the editor's gutter, output in the **Debugger** tab and the input window open. + With JEditor 1.0.27 it runs once per editor tab: to debug again, open the file in + another tab. + * - **Debugger > Show debugger input** + - Opens the debugger's input window again. * - **Clean Result** - - Clears the output panel (both program and shell results). - * - **Stop Program** - - Stops the currently running program process. - -Run Help Submenu -^^^^^^^^^^^^^^^^ - -.. list-table:: - :header-rows: 1 - :widths: 30 70 - - * - Menu Item - - Description - * - **Run Help** - - Displays help information about the program runner. - * - **Shell Help** - - Displays help information about the shell runner. + - Empties the tab's **Code result**. + * - **Stop current program** + - Stops the tab's program, shell command and debugger. + * - **Stop All Program** + - Stops every program, shell command, debugger and ``pip`` run started from these + menus, in any tab. PyBreeze's own runs have their run window's **Stop** button. + * - **Run Help > Run Help** / **Shell Help** + - Tips: check the interpreter, and match the encoding to the shell's. + * - **Run with...** + - Only when a plugin has registered a run configuration (see :doc:`menu_plugins`). + +Run Program, Run Debugger, the **Python Env** entries and PyBreeze's automation runs use +the interpreter chosen under **Python Env > Choose python interpreter**. With none chosen, +Run Program and Run Debugger use a ``venv/`` in the working folder, else Python on +``PATH``; PyBreeze's automation runs and JupyterLab use a ``venv/`` or ``.venv/`` there, +else the Python PyBreeze runs on. Run On Shell uses no interpreter. Text Menu --------- @@ -62,10 +91,27 @@ Text Menu * - Menu Item - Description - * - **Font** - - Opens a font selection dialog to change the editor's default font. - * - **Font Size** - - Opens a dialog to change the editor's default font size. + * - **Font** / **Font Size** + - The font and size of the editors and their **Code result**, in every editor tab. + * - **Word Wrap** + - Wraps long lines in every editor tab (off again at the next start). + * - **Indent Size** + - 2, 4 or 8 spaces: the tab width and indent unit. A file's own indentation wins. + * - **Trim Trailing Whitespace**, **Convert Indentation to Spaces** / **to Tabs** + - The whole document. + * - **Remove Duplicate Lines**, **Reverse Lines**, **Sort Lines (Natural)**, + **Remove Blank Lines**, **Align by Delimiter...** + - The lines the selection covers, at least two. **Align by Delimiter...** asks for the + delimiter (``=`` by default). + * - **Uppercase Selection**, **Lowercase Selection**, **Swap Case**, **Title Case**, + **Naming Style**, **Number Base**, **Encode / Decode** + - The selected text; nothing happens without a selection, or when it cannot be + converted. **Naming Style** gives ``snake_case``, ``camelCase``, ``PascalCase`` or + ``kebab-case``; **Number Base** hexadecimal, decimal or binary; **Encode / Decode** + Base64, URL, HTML and JSON string escaping both ways. + * - **Statistics** + - Lines, words, characters and characters without spaces, of the selection or the + whole document. Check Code Style Menu --------------------- @@ -77,12 +123,20 @@ Check Code Style Menu * - Menu Item - Description * - **yapf** - - Checks and formats the current Python code using the ``yapf`` code formatter. + - Reformats the whole tab with ``yapf`` (Google style); nothing changes on a syntax + error. * - **Reformat JSON** - - Reformats and validates the current content as JSON, applying proper indentation. - -Venv Menu ---------- + - Rewrites the tab as JSON with a 4-space indent and sorted keys; an error is shown in + **Code result**. + * - **Python format check** + - Runs ``pycodestyle`` on the saved ``.py`` file (unsaved edits are not checked) and + lists what it found in the **Format checker** tab. + * - **Format on Save** + - On or off: ``.py`` files are reformatted with ``yapf`` as **Save File**, **Save All** + and **Run Program** write them. The automatic save does not format. + +Python Env Menu +--------------- .. list-table:: :header-rows: 1 @@ -90,22 +144,39 @@ Venv Menu * - Menu Item - Description - * - **Create Venv** - - Creates a Python virtual environment in the current working directory - using the shell runner. - * - **pip upgrade package** - - Upgrades a specified package using pip within the virtual environment. - * - **pip package** - - Installs a specified package using pip within the virtual environment. - -.. note:: - - PyBreeze automatically detects ``.venv`` or ``venv`` directories in your - project and uses the virtual environment's Python interpreter when available. + * - **Create venv** + - Runs ``python -m venv venv`` in the working folder, with the chosen interpreter or + Python on ``PATH``; the output goes to **Code result**. + * - **pip upgrade package** / **pip package** + - Ask for a package name and run ``pip install`` (``-U`` to upgrade) with the chosen + interpreter, or the one in ``venv/``. They need a ``venv/`` in the working folder. + * - **Choose python interpreter** + - Picks the interpreter file. It is saved, and used as described under **Run Menu** + above; the **Install** menu installs into it too. + +Tab and Dock Menus +------------------ + +**Tab** opens a panel as a tab, **Dock** as a dock (see :doc:`ui_overview`): + +- **Add Editor Tab**, **Add Web Browser Tab**, and a docked editor (**Dock > Editor > + New Dock Editor**, which writes its file back when the dock closes) +- **Console Widget** -- an interactive shell (``cmd``, PowerShell, ``bash`` or ``sh``) +- **Toggle Split View** (the same document twice) and **Toggle Minimap**, for the tab in + front +- **Snippet Editor** -- the snippets in ``.jeditor/snippets.json`` +- **Tools Tab** -- IPython (Jupyter, inside the IDE's own Python), the variable inspector, + FrontEngine, ChatUI, the TODO panel, the outline of the current Python file, and + JupyterLab (see :doc:`jupyter_lab`) +- **Git Tab** -- the Git client, the branch tree viewer, the code diff viewer, and the + current file's diff against ``HEAD`` or the staged version +- **Dock > Tools** also has **Problems** (``ruff`` findings) and **Tests** (``pytest`` in + the working folder) UI Style Menu ------------- -Contains a list of available Qt Material themes. Clicking any theme immediately -applies it to the entire application. See :doc:`ui_overview` for the full list -of available themes. +The Qt Material themes (see :doc:`ui_overview`): clicking one applies it at once, and it +is saved for the next start. **Show Indent Guides** and **Show Trailing Whitespace** turn +those marks in the editors on and off, and **Keyboard Shortcuts...** rebinds the editor's +commands. diff --git a/docs/source/Eng/ui_overview.rst b/docs/source/Eng/ui_overview.rst index 36955b01..a32f96df 100644 --- a/docs/source/Eng/ui_overview.rst +++ b/docs/source/Eng/ui_overview.rst @@ -3,8 +3,8 @@ UI Overview .. image:: images/ui.png -PyBreeze provides a tabbed, dock-based interface built on PySide6 (Qt for Python). -The main window is composed of several key areas. +PyBreeze provides a tabbed, dock-based interface built on PySide6 (Qt for Python) and +the JEditor editor engine. The main window is composed of several key areas. Main Window Layout ------------------ @@ -12,22 +12,28 @@ Main Window Layout Menu Bar ^^^^^^^^ -Located at the top of the window. Contains all menus for file operations, code execution, -automation modules, tool installation, SSH, AI tools, plugins, and more. - -The menu bar includes these top-level menus (from left to right): - -- **File** -- Open, save files, and set encoding -- **Run** -- Execute code, run on shell, stop programs -- **Text** -- Font and font size settings -- **Check Code Style** -- Code formatting tools (yapf, JSON) -- **Venv** -- Virtual environment management -- **UI Style** -- Theme switching -- **Automation** -- All automation module menus (AutoControl, APITestka, WebRunner, etc.) -- **Install** -- Install automation packages and build tools -- **Tools** -- SSH client, AI tools, diagram editor -- **Plugins** -- Plugin browser and loaded plugins -- **Run With** -- Run files with different compilers/interpreters +The menu bar holds these top-level menus, from left to right: + +- **File** -- new, open and save files, open a folder, recent files, font, encoding and + line endings (see :doc:`menu_file_run_text`) +- **Run** -- run the current file with Python, or its text as a shell command, the debugger, + stop a run; + **Run with...** when a plugin has registered a run configuration +- **Text** -- font, word wrap, indentation and text transformations +- **Check Code Style** -- ``yapf``, JSON reformatting, the Python format check and format + on save +- **Python Env** -- create a virtual environment, ``pip``, and choose the interpreter runs + use +- **Tab** -- open editor, browser, console, Git and tool tabs (JupyterLab is under + **Tools Tab**, see :doc:`jupyter_lab`) +- **Dock** -- open the same kinds of panels, and PyBreeze's tools, as docks +- **UI Style** -- the theme, indent guides, trailing whitespace and keyboard shortcuts +- **Language** -- the interface language +- **Automation** -- the automation modules (see :doc:`menu_automation`) +- **Install** -- install automation packages and build tools (see :doc:`menu_install`) +- **Tools** -- the SSH client, AI tools, diagram editor and HTTP / API utilities (see + :doc:`menu_tools`) +- **Plugins** -- the Plugin Browser and the loaded plugins (see :doc:`menu_plugins`) File Tree ^^^^^^^^^ @@ -38,6 +44,8 @@ directory. You can: - Browse files and folders - Double-click to open files in the editor - Right-click any file or folder to access the context menu (see below) +- Press **F2** to rename, or **Delete** to delete, the item in focus while the tree has + the focus File Tree Context Menu """""""""""""""""""""" @@ -52,100 +60,116 @@ Right-clicking the file tree opens a context menu with the following actions: - Description * - **New File** - Prompts for a name and creates an empty file in the clicked directory - (or the directory containing the clicked file). + (or the directory containing the clicked file). A name with a drive, a leading + slash, ``..`` or ``:`` is refused, and so is one that already exists. * - **New Folder** - - Prompts for a name and creates a new directory. + - Prompts for a name and creates a new directory, with the same checks. * - **Rename** - - Renames the selected file or folder. If the file is currently open in - an editor tab, the tab title and the editor's stored path are updated - to match. + - Renames the selected file or folder. Editor tabs open on it, or on a file inside + the folder, follow it to the new name. * - **Delete** - - Deletes the selected file or folder after a confirmation dialog. If - the file is open in an editor tab, the tab is closed first. + - Asks first, with **No** as the default, then moves the item to the trash (the + Recycle Bin on Windows). Where there is no trash, as on some network drives, it + asks again before deleting for good. A link is removed itself, not what it points + to. Editor tabs whose file is gone are closed. * - **Copy Path** - Copies the absolute path of the selected item to the clipboard. * - **Copy Relative Path** - Copies the path relative to the file tree's root directory. - * - **Reveal in Explorer** - - Opens the selected item's containing folder in the platform file - manager (Explorer on Windows, Finder on macOS, ``xdg-open`` on Linux). + * - **Reveal in File Explorer** + - Shows the item in the platform file manager: selected in Explorer on Windows and + in Finder on macOS; on Linux, ``xdg-open`` opens its folder. Code Editor (Tab Widget) ^^^^^^^^^^^^^^^^^^^^^^^^^ The central area uses a tabbed interface. Each opened file gets its own tab. -Additional tool tabs (SSH, AI, JupyterLab, automation GUIs) can also be opened here. +Additional tool tabs (SSH, AI, JupyterLab, the diagram editor, the HTTP / API utilities, +automation GUIs) can also be opened here. Features: -- Syntax highlighting for Python and automation module keywords +- Syntax highlighting for Python and for the languages JEditor colours (C, C++, Go, Java, + JavaScript, JSON, Rust, shell, SQL, TOML, TypeScript, YAML and more). PyBreeze registers + its automation keywords for ``.json``, ``.yml`` and ``.yaml``, but JEditor highlights + those files with its own rules, which leave registered keywords out. - Multiple file editing with tabs -- Extendable with custom tabs via ``EDITOR_EXTEND_TAB`` +- Extendable with custom tabs via ``EDITOR_EXTEND_TAB`` (see :doc:`how_to_extend_ui`) +- A tool tab with unsaved work (a prompt editor, the diagram editor) asks before it + closes, and so does closing the IDE Output Panel ^^^^^^^^^^^^ -Located at the bottom of the window. Displays: +Each editor tab has a panel below it with the tabs **Code result**, **Format checker**, +**Debugger**, **Terminal**, **Variable Inspector** and **Git Client**: -- Program execution output -- Shell command results -- Error messages +- **Code result** shows the output of **Run Program** and **Run On Shell**, errors in the + theme's error colour, and the IDE's log messages (warnings and errors only); +- **Format checker** lists what **Check Code Style > Python format check** found; +- **Debugger** is where **Run Debugger** runs. -Code Output Window -^^^^^^^^^^^^^^^^^^ +Run Window +^^^^^^^^^^ -When running automation scripts, a separate **Code Output Window** opens to display -the execution results. This window: +An automation script, a batch of them, a package install or a **Run with...** run each +opens a window of its own for its output. This window: -- Shows real-time output from subprocess execution -- Is read-only -- Sizes itself based on screen dimensions -- Can be closed independently of the main window +- Is titled with what it runs (the package and the file, for instance) +- Shows the output as it arrives, errors in the error colour, in a fixed-pitch font, and + keeps the last 10,000 lines +- Has a **Stop** button, enabled while the run goes on +- Can be closed while the run goes on: the run continues, and closing the IDE stops it +- Sizes itself to a third of the screen Dock Widgets ^^^^^^^^^^^^ -Several tools can be opened as **dock widgets** instead of tabs, allowing you to -arrange them freely around the main window: +Panels can be opened as **dock widgets** instead of tabs from the **Dock** menu, and +arranged freely around the main window: -- SSH Client Dock -- AI Code Review Dock -- CoT Prompt Editor Dock -- Skill Prompt Editor Dock -- Skill Send Dock -- Diagram Editor Dock +- **Editor** -- a docked editor, IPython (Jupyter) and the variable inspector +- **Git** -- the Git client, branch tree viewer and code diff viewer +- **AI** -- Chat UI, AI Code Review, CoT Prompt Editor, CoT Code Review, Skill Prompt + Editor and Skill Send +- **Tools** -- a browser, FrontEngine, the console, the TODO panel, Problems, Tests and + the outline +- **SSH** -- the SSH client +- the diagram editor and each HTTP / API utility, at the menu's top level -Dock widgets can be dragged, resized, and snapped to any edge of the main window. +Dock widgets can be dragged, resized, floated, stacked and snapped to any edge of the +main window. Theme System ------------ PyBreeze uses `qt_material `_ for theming. -Available themes include: +The **UI Style** menu lists these themes: - ``dark_amber.xml`` (default) -- ``dark_teal.xml`` - ``dark_blue.xml`` - ``dark_cyan.xml`` - ``dark_lightgreen.xml`` - ``dark_pink.xml`` - ``dark_purple.xml`` - ``dark_red.xml`` +- ``dark_teal.xml`` - ``dark_yellow.xml`` - ``light_amber.xml`` - ``light_blue.xml`` - ``light_cyan.xml`` +- ``light_cyan_500.xml`` - ``light_lightgreen.xml`` - ``light_pink.xml`` - ``light_purple.xml`` -- ``light_red.xml`` -- ``light_yellow.xml`` -You can switch themes via the **UI Style** menu in the menu bar, or set the theme -at launch time: +Clicking one applies it to the whole application. The theme can also be set at launch +time: .. code-block:: python + from pybreeze import start_editor + start_editor(theme="dark_teal.xml") The theme picked from **UI Style** is saved and used at the next start. A theme given to @@ -154,11 +178,13 @@ The theme picked from **UI Style** is saved and used at the next start. A theme Multi-Language Support ---------------------- -PyBreeze supports multiple UI languages: +The **Language** menu offers: - **English** (default) -- **Traditional Chinese** (繁體中文) -- Additional languages can be added via plugins +- **繁體中文** (Traditional Chinese) +- **日本語** (Japanese) and **简体中文** (Simplified Chinese), which are JEditor's: picked, + JEditor's own menus change and PyBreeze's strings stay in English +- languages added by translation plugins (see :doc:`menu_plugins`) -All UI strings are managed through a centralized language dictionary that can be -extended with custom translations. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices +follow the chosen language. diff --git a/docs/source/Zh/getting_started.rst b/docs/source/Zh/getting_started.rst index 26bdf9f0..cbb5d587 100644 --- a/docs/source/Zh/getting_started.rst +++ b/docs/source/Zh/getting_started.rst @@ -4,8 +4,9 @@ 系統需求 -------- -- Python 3.10 或更高版本 +- Python 3.10 到 3.14 - pip(Python 套件管理器) +- Windows、macOS 或 Linux;PySide6 會隨 PyBreeze 一起安裝 安裝 ---- @@ -16,6 +17,18 @@ pip install pybreeze +或從原始碼安裝: + +.. code-block:: bash + + git clone https://github.com/Integration-Automation/PyBreeze.git + cd PyBreeze + pip install -r requirements.txt + +兩種方式都會一併安裝自動化模組(AutoControl、APITestka、WebRunner、LoadDensity、FileAutomation、 +MailThunder、TestPioneer)、paramiko 與 JupyterLab。之後可以從 **Install** 選單把它們升級到執行時使用的直譯器 +(見 :doc:`menu_install`)。 + 啟動 PyBreeze ------------- @@ -57,13 +70,24 @@ * - ``debug_mode`` - bool - ``False`` - - 10 秒後自動關閉(用於 CI 測試) + - 10 秒後自行關閉(用於啟動測試) * - ``theme`` - str 或 None - ``None`` - Qt Material 主題名稱。它會取代從 **UI Style** 選的主題,並成為選定的主題。``None`` 表示用選定的主題 (還沒選過時是 ``dark_amber.xml``)。 +工作資料夾 +---------- + +請從你的專案資料夾啟動 PyBreeze。它啟動時所在的資料夾,或之後以 **File > Open Folder** 開啟的資料夾,就是: + +- 檔案樹開啟的位置; +- 尋找 ``venv/`` 或 ``.venv/`` 的位置,在 **Python Env** 沒有選直譯器時用它執行腳本; +- **Create ... Project** 與 TestPioneer 範本寫入的位置。 + +外掛只在啟動時載入一次,來源是 PyBreeze 啟動時所在資料夾中的 ``jeditor_plugins/``\ (見 :doc:`menu_plugins`)。 + 首次啟動 -------- @@ -72,6 +96,6 @@ PyBreeze 啟動後,主視窗會以最大化方式開啟,包含: 1. **選單列** -- 位於頂部,包含所有可用選單 2. **檔案樹** -- 位於左側,用於專案導覽 3. **程式碼編輯器**\ (分頁式)-- 位於中央,用於編輯檔案 -4. **輸出面板** -- 位於底部,顯示執行結果 +4. **輸出面板** -- 位於編輯器下方,其中的 **Code result** 分頁顯示 **Run Program** 與 **Run On Shell** 的輸出 PyBreeze 繼承了 **JEditor** 的核心編輯器功能,並擴充了自動化專用的選單、工具和整合功能。 diff --git a/docs/source/Zh/menu_file_run_text.rst b/docs/source/Zh/menu_file_run_text.rst index 7161b1c5..73fac4e2 100644 --- a/docs/source/Zh/menu_file_run_text.rst +++ b/docs/source/Zh/menu_file_run_text.rst @@ -1,7 +1,11 @@ -File、Run、Text 及其他基礎選單 +File、Run、Text 與其他基礎選單 ============================== -這些選單繼承自 JEditor 基礎編輯器引擎,提供核心的編輯和執行功能。 +這些選單來自 PyBreeze 所建構的 JEditor 編輯器引擎。幾乎每個項目都作用在前景的編輯分頁上, +前景不是編輯分頁時什麼都不做。 + +JEditor 把這些設定存在工作資料夾的 ``.jeditor/user_setting.json``,所以每個專案資料夾各有一份; +每分鐘與關閉 IDE 時儲存。已有檔案的編輯分頁也會每隔幾秒自動存回檔案。 File 選單 --------- @@ -12,13 +16,24 @@ File 選單 * - 選單項目 - 說明 + * - **New File** + - 詢問名稱與位置,在那裡建立空檔案(不會開啟它)。 * - **Open File** - - 開啟檔案對話框選擇檔案,將檔案內容載入到程式碼編輯區。 + - 把檔案開進前景的編輯分頁,取代它原本顯示的內容。 + * - **Open Folder** + - 把一個資料夾設為工作資料夾:檔案樹顯示它,並載入它的設定(見 :doc:`getting_started`)。 * - **Save File** - - 將目前程式碼編輯區的內容儲存到檔案。 - * - **Encoding** - - 開啟對話框選擇程式執行器和 Shell 執行器的編碼 - (如 UTF-8、ASCII、Big5)。 + - 開啟從工作資料夾開始的 **Save As** 對話框,把分頁寫到選定的位置。 + * - **Recent Files** + - 最近開啟的檔案,點選後在新分頁開啟。清單在啟動時重建。 + * - **Font** / **Font Size** + - 整個視窗的字型與大小:選單、樹狀檢視與面板。 + * - **Encodings** + - 分頁檔案的編碼。分頁沒有未存的編輯時,會以這個編碼重新讀取檔案;下次存檔也用它寫入。 + * - **Line Endings** + - 分頁檔案下次存檔用的 ``LF``、``CRLF`` 或 ``CR``。 + * - **Save All** + - 寫入每個已有檔案的編輯分頁。 Run 選單 -------- @@ -29,28 +44,36 @@ Run 選單 * - 選單項目 - 說明 - * - **Run Program** - - 使用程式執行器(Python 直譯器)執行目前程式碼編輯區的內容。 - * - **Run On Shell** - - 使用系統 Shell 執行目前程式碼編輯區的內容。 + * - **Run Program > Run Program** + - 開啟 **Save As** 對話框寫入分頁,再以 Python 執行檔案;輸出到分頁的 **Code result**。 + 每個分頁一次只能執行一個程式。 + * - **Run Program > Show program input** + - 一個小視窗,輸入的文字會送到執行中程式的標準輸入。 + * - **Run On Shell > Run On Shell** + - 把分頁的文字原樣當成一個 Shell 指令執行(Windows 上是 ``cmd.exe``);輸出到 **Code result**。 + * - **Run On Shell > Show shell input** + - 同樣的輸入視窗,給 Shell 用。 + * - **Debugger > Run Debugger** + - 開啟 **Save As** 對話框後,以 ``pdb`` 執行檔案,並帶入編輯器邊欄設定的中斷點;輸出在 **Debugger** + 分頁,輸入視窗會自動開啟。JEditor 1.0.27 中每個編輯分頁只能執行一次:要再除錯,請在另一個分頁開啟該檔案。 + * - **Debugger > Show debugger input** + - 再次開啟除錯器的輸入視窗。 * - **Clean Result** - - 清除輸出面板(包含程式和 Shell 的結果)。 - * - **Stop Program** - - 停止目前正在執行的程式。 - -Run Help 子選單 -^^^^^^^^^^^^^^^ - -.. list-table:: - :header-rows: 1 - :widths: 30 70 - - * - 選單項目 - - 說明 - * - **Run Help** - - 顯示程式執行器的說明資訊。 - * - **Shell Help** - - 顯示 Shell 執行器的說明資訊。 + - 清空分頁的 **Code result**。 + * - **Stop current program** + - 停止分頁的程式、Shell 指令與除錯器。 + * - **Stop All Program** + - 停止從這些選單啟動、在任何分頁中執行的程式、Shell 指令、除錯器與 ``pip``。 + PyBreeze 自己的執行由執行視窗的 **Stop** 按鈕停止。 + * - **Run Help > Run Help** / **Shell Help** + - 提示:確認直譯器,並讓編碼與 Shell 的一致。 + * - **Run with...** + - 只在有外掛註冊了執行設定時出現(見 :doc:`menu_plugins`)。 + +Run Program、Run Debugger、**Python Env** 的項目與 PyBreeze 的自動化執行,都使用在 +**Python Env > Choose python interpreter** 選的直譯器。沒有選時,Run Program 與 Run Debugger 用工作資料夾中的 +``venv/``,否則用 ``PATH`` 上的 Python;PyBreeze 的自動化執行與 JupyterLab 用那裡的 ``venv/`` 或 ``.venv/``, +否則用執行 PyBreeze 的那個 Python。Run On Shell 不使用直譯器。 Text 選單 --------- @@ -61,10 +84,24 @@ Text 選單 * - 選單項目 - 說明 - * - **Font** - - 開啟字型選擇對話框,變更編輯器的預設字型。 - * - **Font Size** - - 開啟對話框變更編輯器的預設字型大小。 + * - **Font** / **Font Size** + - 每個編輯分頁中編輯器與 **Code result** 的字型與大小。 + * - **Word Wrap** + - 在每個編輯分頁中自動換行(下次啟動時又會關閉)。 + * - **Indent Size** + - 2、4 或 8 個空白:Tab 寬度與縮排單位。檔案本身的縮排優先。 + * - **Trim Trailing Whitespace**、**Convert Indentation to Spaces** / **to Tabs** + - 整份文件。 + * - **Remove Duplicate Lines**、**Reverse Lines**、**Sort Lines (Natural)**、 + **Remove Blank Lines**、**Align by Delimiter...** + - 選取範圍涵蓋的行,至少兩行。**Align by Delimiter...** 會詢問分隔符號(預設 ``=``)。 + * - **Uppercase Selection**、**Lowercase Selection**、**Swap Case**、**Title Case**、 + **Naming Style**、**Number Base**、**Encode / Decode** + - 選取的文字;沒有選取、或無法轉換時什麼都不做。**Naming Style** 轉成 ``snake_case``、``camelCase``、 + ``PascalCase`` 或 ``kebab-case``;**Number Base** 轉成十六進位、十進位或二進位;**Encode / Decode** + 做 Base64、URL、HTML 與 JSON 字串的雙向跳脫。 + * - **Statistics** + - 選取範圍或整份文件的行數、字數、字元數與不含空白的字元數。 Check Code Style 選單 --------------------- @@ -76,12 +113,17 @@ Check Code Style 選單 * - 選單項目 - 說明 * - **yapf** - - 使用 ``yapf`` 程式碼格式化工具檢查並格式化目前的 Python 程式碼。 + - 以 ``yapf``\ (Google 風格)重新排版整個分頁;有語法錯誤時不會改動。 * - **Reformat JSON** - - 重新格式化並驗證目前內容為 JSON,套用適當的縮排。 + - 把分頁改寫成 4 格縮排、鍵排序過的 JSON;錯誤顯示在 **Code result**。 + * - **Python format check** + - 對存檔後的 ``.py`` 檔執行 ``pycodestyle``\ (不檢查未存的編輯),結果列在 **Format checker** 分頁。 + * - **Format on Save** + - 開關:**Save File**、**Save All** 與 **Run Program** 寫入 ``.py`` 檔時,先以 ``yapf`` 排版。 + 自動存檔不會排版。 -Venv 選單 ---------- +Python Env 選單 +--------------- .. list-table:: :header-rows: 1 @@ -89,20 +131,31 @@ Venv 選單 * - 選單項目 - 說明 - * - **Create Venv** - - 在目前工作目錄中使用 Shell 執行器建立 Python 虛擬環境。 - * - **pip upgrade package** - - 在虛擬環境中使用 pip 升級指定的套件。 - * - **pip package** - - 在虛擬環境中使用 pip 安裝指定的套件。 - -.. note:: - - PyBreeze 會自動偵測專案中的 ``.venv`` 或 ``venv`` 目錄, - 並在可用時使用虛擬環境的 Python 直譯器。 + * - **Create venv** + - 在工作資料夾執行 ``python -m venv venv``,用選定的直譯器或 ``PATH`` 上的 Python;輸出到 **Code result**。 + * - **pip upgrade package** / **pip package** + - 詢問套件名稱後執行 ``pip install``\ (升級時加 ``-U``),用選定的直譯器或 ``venv/`` 裡的那一個。 + 工作資料夾中必須有 ``venv/``。 + * - **Choose python interpreter** + - 選擇直譯器檔案。它會被儲存,並依上方 **Run 選單** 所述使用;**Install** 選單也安裝到它裡面。 + +Tab 與 Dock 選單 +---------------- + +**Tab** 以分頁開啟面板,**Dock** 以停靠面板開啟(見 :doc:`ui_overview`): + +- **Add Editor Tab**、**Add Web Browser Tab**,以及停靠式編輯器(**Dock > Editor > New Dock Editor**, + 關閉停靠面板時寫回它的檔案) +- **Console Widget** -- 互動式 Shell(``cmd``、PowerShell、``bash`` 或 ``sh``) +- **Toggle Split View**\ (同一份文件顯示兩次)與 **Toggle Minimap**,作用在前景分頁 +- **Snippet Editor** -- ``.jeditor/snippets.json`` 中的程式碼片段 +- **Tools Tab** -- IPython(Jupyter,在 IDE 自己的 Python 中)、變數檢視器、FrontEngine、ChatUI、TODO 面板、 + 目前 Python 檔的大綱,以及 JupyterLab(見 :doc:`jupyter_lab`) +- **Git Tab** -- Git 用戶端、分支樹檢視器、程式碼差異檢視器,以及目前檔案與 ``HEAD`` 或暫存版本的差異 +- **Dock > Tools** 另有 **Problems**\ (``ruff`` 找到的問題)與 **Tests**\ (在工作資料夾執行 ``pytest``) UI Style 選單 ------------- -包含可用的 Qt Material 主題列表。點擊任何主題即可立即套用到整個應用程式。 -完整的主題列表請參閱 :doc:`ui_overview`。 +Qt Material 主題(見 :doc:`ui_overview`):點選後立即套用,並儲存供下次啟動使用。**Show Indent Guides** 與 +**Show Trailing Whitespace** 開關編輯器中的這兩種標示,**Keyboard Shortcuts...** 可重新綁定編輯器的指令。 diff --git a/docs/source/Zh/ui_overview.rst b/docs/source/Zh/ui_overview.rst index 77fdb1f4..d71fbcd0 100644 --- a/docs/source/Zh/ui_overview.rst +++ b/docs/source/Zh/ui_overview.rst @@ -3,31 +3,33 @@ UI 總覽 .. image:: images/ui.png -PyBreeze 提供基於分頁和停靠面板的介面,使用 PySide6(Qt for Python)建構。 +PyBreeze 提供基於分頁和停靠面板的介面,使用 PySide6(Qt for Python)與 JEditor 編輯器引擎建構。 主視窗由以下幾個主要區域組成。 +本指南的選單與按鈕名稱以英文介面為準。在 **Language** 選單選「繁體中文」後,介面會顯示對應的中文名稱, +例如 **Tools** 是「工具」、**Dock** 是「區域」、**Automation** 是「自動化」。 + 主視窗佈局 ---------- 選單列 ^^^^^^ -位於視窗頂部,包含所有檔案操作、程式碼執行、自動化模組、工具安裝、 -SSH、AI 工具、外掛等選單。 - 選單列包含以下頂層選單(由左至右): -- **File** -- 開啟、儲存檔案,設定編碼 -- **Run** -- 執行程式碼、在 Shell 中執行、停止程式 -- **Text** -- 字型和字型大小設定 -- **Check Code Style** -- 程式碼格式化工具(yapf、JSON) -- **Venv** -- 虛擬環境管理 -- **UI Style** -- 主題切換 -- **Automation** -- 所有自動化模組選單(AutoControl、APITestka、WebRunner 等) -- **Install** -- 安裝自動化套件和建置工具 -- **Tools** -- SSH 用戶端、AI 工具、架構圖編輯器 -- **Plugins** -- 外掛瀏覽器和已載入外掛 -- **Run With** -- 使用不同編譯器/直譯器執行檔案 +- **File** -- 新增、開啟與儲存檔案,開啟資料夾、最近的檔案、字型、編碼與行尾(見 :doc:`menu_file_run_text`) +- **Run** -- 以 Python 執行目前的檔案,或把它的文字當成 Shell 指令執行、除錯器、停止執行;有外掛註冊執行設定時另有 **Run with...** +- **Text** -- 字型、自動換行、縮排與文字轉換 +- **Check Code Style** -- ``yapf``、JSON 重新排版、Python 格式檢查與存檔時自動格式化 +- **Python Env** -- 建立虛擬環境、``pip``,以及選擇執行時使用的直譯器 +- **Tab** -- 開啟編輯器、瀏覽器、主控台、Git 與工具分頁(JupyterLab 在 **Tools Tab** 下,見 :doc:`jupyter_lab`) +- **Dock** -- 以停靠面板開啟同類的面板,以及 PyBreeze 的工具 +- **UI Style** -- 主題、縮排參考線、行尾空白與鍵盤快捷鍵 +- **Language** -- 介面語言 +- **Automation** -- 自動化模組(見 :doc:`menu_automation`) +- **Install** -- 安裝自動化套件與建置工具(見 :doc:`menu_install`) +- **Tools** -- SSH 用戶端、AI 工具、架構圖編輯器與 HTTP / API 小工具(見 :doc:`menu_tools`) +- **Plugins** -- 外掛瀏覽器與已載入的外掛(見 :doc:`menu_plugins`) 檔案樹 ^^^^^^ @@ -37,6 +39,7 @@ SSH、AI 工具、外掛等選單。 - 瀏覽檔案和資料夾 - 雙擊在編輯器中開啟檔案 - 右鍵任何檔案或資料夾以開啟功能選單(詳見下方) +- 檔案樹有焦點時,按 **F2** 重新命名、按 **Delete** 刪除焦點所在的項目 檔案樹右鍵選單 """""""""""""" @@ -50,97 +53,104 @@ SSH、AI 工具、外掛等選單。 * - 動作 - 說明 * - **New File** - - 跳出輸入框詢問檔名,在按右鍵的目錄下(或被點擊檔案所在目錄) - 建立空檔案。 + - 跳出輸入框詢問檔名,在按右鍵的目錄下(或被點擊檔案所在目錄)建立空檔案。 + 含磁碟機代號、開頭斜線、``..`` 或 ``:`` 的名稱會被拒絕,已存在的名稱也是。 * - **New Folder** - - 跳出輸入框詢問資料夾名稱,建立新目錄。 + - 跳出輸入框詢問資料夾名稱,建立新目錄,檢查方式相同。 * - **Rename** - - 重新命名選取的檔案或資料夾。若該檔案正以分頁形式開啟在編輯器中, - 分頁標題與編輯器內部的路徑會同步更新。 + - 重新命名選取的檔案或資料夾。開在這個檔案、或資料夾內檔案上的編輯器分頁會跟著改到新名稱。 * - **Delete** - - 顯示確認對話框後刪除選取的檔案或資料夾。若該檔案正以分頁開啟, - 會先關閉分頁。 + - 先詢問(預設為 **No**),再把項目移到垃圾桶(Windows 的資源回收筒)。沒有垃圾桶的地方 + (例如某些網路磁碟)會再問一次,才永久刪除。連結只刪除連結本身,不動它指向的東西。 + 檔案已不存在的編輯器分頁會關閉。 * - **Copy Path** - 將選取項目的絕對路徑複製到剪貼簿。 * - **Copy Relative Path** - - 複製相對於檔案樹根目錄的相對路徑。 - * - **Reveal in Explorer** - - 在系統檔案管理器中開啟選取項目所在資料夾(Windows 的檔案總管、 - macOS 的 Finder、Linux 的 ``xdg-open``)。 + - 複製相對於檔案樹根目錄的路徑。 + * - **Reveal in File Explorer** + - 在作業系統的檔案管理員中顯示這個項目:Windows 的檔案總管與 macOS 的 Finder 會選取它; + Linux 則以 ``xdg-open`` 開啟它所在的資料夾。 程式碼編輯器(分頁元件) ^^^^^^^^^^^^^^^^^^^^^^^^ -中央區域使用分頁介面。每個開啟的檔案都有自己的分頁。 -額外的工具分頁(SSH、AI、JupyterLab、自動化 GUI)也可以在此開啟。 +中央區域使用分頁介面,每個開啟的檔案都有自己的分頁。其他工具分頁(SSH、AI、JupyterLab、 +架構圖編輯器、HTTP / API 小工具、自動化 GUI)也可以在這裡開啟。 功能: -- Python 和自動化模組關鍵字的語法高亮 -- 多檔案分頁編輯 -- 可透過 ``EDITOR_EXTEND_TAB`` 擴充自訂分頁 +- Python 與 JEditor 會上色的語言(C、C++、Go、Java、JavaScript、JSON、Rust、Shell、SQL、TOML、 + TypeScript、YAML 等)的語法高亮。PyBreeze 為 ``.json``、``.yml``、``.yaml`` 註冊了自動化關鍵字, + 但 JEditor 用自己的規則為這些檔案上色,不包含註冊的關鍵字。 +- 多分頁編輯多個檔案 +- 可透過 ``EDITOR_EXTEND_TAB`` 擴充自訂分頁(見 :doc:`how_to_extend_ui`) +- 有未儲存內容的工具分頁(提示詞編輯器、架構圖編輯器)關閉前會先詢問,關閉 IDE 時也是 輸出面板 ^^^^^^^^ -位於視窗底部,顯示: +每個編輯器分頁下方都有一個面板,包含 **Code result**、**Format checker**、**Debugger**、**Terminal**、 +**Variable Inspector** 與 **Git Client** 這幾個分頁: -- 程式執行輸出 -- Shell 命令結果 -- 錯誤訊息 +- **Code result** 顯示 **Run Program** 與 **Run On Shell** 的輸出,錯誤以主題的錯誤色呈現, + 也會顯示 IDE 的日誌訊息(只有警告與錯誤); +- **Format checker** 列出 **Check Code Style > Python format check** 找到的問題; +- **Debugger** 是 **Run Debugger** 執行的地方。 -程式碼輸出視窗 -^^^^^^^^^^^^^^ +執行視窗 +^^^^^^^^ -執行自動化腳本時,會開啟獨立的 **程式碼輸出視窗** 顯示執行結果。此視窗: +自動化腳本、一批腳本、套件安裝或 **Run with...** 的每一次執行,都會開一個自己的視窗顯示輸出。這個視窗: -- 即時顯示子程序的執行輸出 -- 唯讀模式 -- 根據螢幕大小自動調整尺寸 -- 可獨立於主視窗關閉 +- 標題說明它執行的東西(例如套件與檔案) +- 輸出一到就顯示,錯誤以錯誤色、等寬字型呈現,並保留最後 10,000 行 +- 有 **Stop** 按鈕,執行期間可以按 +- 執行中也可以關閉視窗:執行會繼續,關閉 IDE 時才會停止 +- 大小為螢幕的三分之一 停靠面板 ^^^^^^^^ -以下工具可以作為 **停靠面板** 開啟(而非分頁),讓您自由排列: +面板可以從 **Dock** 選單以 **停靠面板** 開啟,而不是分頁,並可在主視窗周圍自由排列: -- SSH 用戶端停靠面板 -- AI 程式碼審查停靠面板 -- CoT 提示詞編輯器停靠面板 -- Skill 提示詞編輯器停靠面板 -- Skill 傳送 GUI 停靠面板 -- 架構圖編輯器停靠面板 +- **Editor** -- 停靠式編輯器、IPython(Jupyter)與變數檢視器 +- **Git** -- Git 用戶端、分支樹檢視器與程式碼差異檢視器 +- **AI** -- Chat UI、AI Code Review、CoT Prompt Editor、CoT Code Review、Skill Prompt Editor 與 Skill Send +- **Tools** -- 瀏覽器、FrontEngine、主控台、TODO 面板、Problems、Tests 與大綱 +- **SSH** -- SSH 用戶端 +- 架構圖編輯器與每個 HTTP / API 小工具,在選單的最上層 -停靠面板可以拖曳、調整大小,並固定到主視窗的任何邊緣。 +停靠面板可以拖曳、調整大小、浮動、堆疊,並停靠到主視窗的任何邊緣。 主題系統 -------- -PyBreeze 使用 `qt_material `_ 進行主題設定。 -可用主題包括: +PyBreeze 使用 `qt_material `_ 提供主題。 +**UI Style** 選單列出這些主題: - ``dark_amber.xml``\ (預設) -- ``dark_teal.xml`` - ``dark_blue.xml`` - ``dark_cyan.xml`` - ``dark_lightgreen.xml`` - ``dark_pink.xml`` - ``dark_purple.xml`` - ``dark_red.xml`` +- ``dark_teal.xml`` - ``dark_yellow.xml`` - ``light_amber.xml`` - ``light_blue.xml`` - ``light_cyan.xml`` +- ``light_cyan_500.xml`` - ``light_lightgreen.xml`` - ``light_pink.xml`` - ``light_purple.xml`` -- ``light_red.xml`` -- ``light_yellow.xml`` -您可以透過選單列中的 **UI Style** 選單切換主題,或在啟動時設定: +點擊任一主題會立即套用到整個應用程式。也可以在啟動時指定主題: .. code-block:: python + from pybreeze import start_editor + start_editor(theme="dark_teal.xml") 從 **UI Style** 選的主題會被儲存,下次啟動時使用。傳給 ``start_editor`` 的主題會取代它,並改存成這一個。 @@ -148,10 +158,11 @@ PyBreeze 使用 `qt_material `_ 進行 多語言支援 ---------- -PyBreeze 支援多種 UI 語言: +**Language** 選單提供: - **English**\ (預設) - **繁體中文** -- 可透過外掛新增其他語言 +- **日本語** 與 **简体中文**,這兩個是 JEditor 的:選了之後 JEditor 自己的選單會改變,PyBreeze 的字串則維持英文 +- 翻譯外掛新增的語言(見 :doc:`menu_plugins`) -所有 UI 字串透過集中式語言字典管理,可擴充自訂翻譯。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息,都會跟著所選的語言顯示。 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 1f15198f..2c983672 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -641,3 +641,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_start_theme.py` (new), `test/test_utils/started_window.py` (`saved_settings` and `build`), `test/test_utils/test_jeditor_contract.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`, `architecture_explore.md`, `docs/source/Eng/getting_started.rst`, `docs/source/Zh/getting_started.rst`, `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst` - **Checks**: `test_start_theme.py` starts the real window in a child and reads the theme qt_material applied: a given theme is shown and saved, and replaces a saved one (both failed before the change, showing the saved theme); with none given, the saved one, or `dark_amber.xml`, is shown. The contract, start-up language, facade and GC tests pass (81); both start-up tests exit 0; the Sphinx build gives no warning. `test/test_utils` collects 2601 tests in 144 files; `ruff` clean. - **Open items**: none. + +## U-20260925-76 · 2026-09-25 · The Sphinx overview, getting-started and base-menu pages told right · #docs + +- **What**: the audit found the UI Overview page, in both languages, listing menus that are not there (Venv, a top-level Run With) and leaving out Python Env, Tab, Dock and Language; a Delete that deletes (it moves to the trash, after asking with No as the default); Reveal in Explorer (Reveal in File Explorer, which selects the file on Windows and macOS); automation keywords said to be highlighted (`progress.md` #103); an output panel and a "Code Output Window" described as neither is; a dock list of six; and a theme list with `light_red` and `light_yellow`, which the UI Style menu does not offer, and without `light_cyan_500`, which it does. The File, Run, Text page had File > Encoding as a dialog (it is a submenu of codecs), a Stop Program entry (Stop current program, Stop All Program), Font and Font Size as dialogs, a Venv menu (Python Env) without Choose python interpreter, and a venv note that did not say which runs look for `venv/` or `.venv/`. Getting Started said Python "3.10 or higher" (3.10 to 3.14 are the versions tested) and called the output panel by another name. A subagent read JEditor 1.0.27 for what each base-menu entry does; the claims used here were checked against the code, and the debugger's was reproduced. +- **Change**: UI Overview lists the thirteen menus in order, the file tree's keys and menu as they behave, the output panel's six tabs, the run window, the Dock menu's groups, the sixteen themes and the four languages, and tells Chinese readers the guide uses the English labels (Tools is 工具, Dock is 區域). The File, Run, Text page gives every entry of File, Run, Text, Check Code Style, Python Env, Tab, Dock and UI Style what it does, which interpreter each run uses, where settings are kept, and that Save File, Run Program and Run Debugger always ask where to save and Run Debugger runs once per tab with JEditor 1.0.27 (`progress.md` #114, #115, recorded with this entry). Getting Started gives the tested Python versions, installing from source, that the automation modules come with PyBreeze, and what the working folder decides. +- **Files**: `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst`, `docs/source/Eng/getting_started.rst`, `docs/source/Zh/getting_started.rst`, `progress.md` +- **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and no built page shows a stray `**` or double backquote. The Run Debugger limit was reproduced in the real window: after `pdb` quit (exit 0) a second Run Debugger in the tab was refused. +- **Open items**: `progress.md` #114, #115. diff --git a/docs/updates/README.md b/docs/updates/README.md index 083e8040..5c5af10b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-76 | 2026-09-25 | The Sphinx overview, getting-started and base-menu pages told right | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-75 | 2026-09-25 | start_editor's theme is the one the IDE shows | #fix #startup | [2026-09-c](2026-09-c.md) | | U-20260925-74 | 2026-09-25 | The main window opens from its own function | #refactor #startup | [2026-09-c](2026-09-c.md) | | U-20260925-73 | 2026-09-25 | The Sphinx Plugins and extension pages told right | #docs #plugins | [2026-09-c](2026-09-c.md) | @@ -478,4 +479,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 75 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 76 | diff --git a/progress.md b/progress.md index 18ea27dd..fbfd33bd 100644 --- a/progress.md +++ b/progress.md @@ -22,3 +22,5 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#111** [DECIDE] AI Code Review, CoT Code Review and Skill Send refuse an endpoint on this machine or on a private network (`validate_url` in `ai_code_review_gui.py:77`, `code_review_thread.py:33`, `skills_send_gui.py:55`; `url_validation._is_blocked_ip`), so a local model server (Ollama, LM Studio, a prthinker server on localhost) cannot be used from them, although CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` until U-20260925-11. The Security › Network rule rejects loopback and private addresses for every user-supplied URL. Should these panels let the user allow a loopback (or private) endpoint, for example after a confirmation naming the address, or stay public-only? - **#112** [DECIDE] Two of the executable build configs still build the IDE under its old name: `exe/auto_py_to_exe_setting_linux.json` (name AutomationIDE, script `exe/start_automation_editor.py` and icon `exe/je_driver_icon.ico`, neither of which exists any more, all under `C:/CodeWorkspace/Python/AutomationIDE`) and the InstallForge project `exe/automation_ide_setup_config.ifp` (program AutomationIDE 1.0.9, the AutomationIDE repository as its website and licence link, the same icon, `...\AutomationIDE\output\AutomationIDE.exe`). The Windows `exe/auto_py_to_exe_setting.json` builds PyBreeze from `exe/start_pybreeze.py` with `exe/pybreeze_icon.ico` (987252e). Should the two be brought over to PyBreeze the same way (their paths are a build machine's), or deleted if they are no longer used? - **#113** [BLOCKED] A translation plugin's language is listed in the Language menu under its key, not the name it registers: `register_natural_language(key, display_name, word_dict)` (`je_editor/plugins/__init__.py`) writes the dictionary into `language_wrapper.choose_language_dict` but not `display_names`, so `available_languages()` lists it and `display_name()` gives the key (`je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py:47-49`), and the loop over `get_all_natural_languages()` below skips it as already listed. Seen with the Sphinx Plugins page's French example ("French" shown, not "Français") on je_editor 1.0.27. Needs JEditor's `register_natural_language` to call `language_wrapper.register_language(key, word_dict, display_name)`. +- **#114** [BLOCKED] File > Save File, Run Program and Run Debugger open a Save As dialog every time, starting in the working folder with no name filled in, even for a tab that already has a file: each calls `choose_file_get_save_file_path()` (`je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py:96`, `.../run_menu/under_run_menu/build_program_menu.py:95`, `build_debug_menu.py:106`), which always asks, on je_editor 1.0.27. PyBreeze's own Run with... saves in place (`save_current_file_for_run`). Needs JEditor to write a tab with a file where it is, and ask only for one without; the Sphinx File/Run/Text page says what it does now. +- **#115** [BLOCKED] Run Debugger works once per editor tab: when the `pdb` run ends, `ExecManager.full_exit_program()` clears the tab's `exec_program`, not `exec_python_debugger` (`je_editor/pyside_ui/code/code_process/code_exec.py:228`), and nothing else clears it, not Stop current program (`build_run_menu.py:117`) or Stop All, so the next Run Debugger in that tab says a program is still running (`build_debug_menu.py:102`). Reproduced on je_editor 1.0.27 in the real window: after `pdb` quit (exit 0) the slot was still set and a second Run Debugger was refused. Needs the debugger's manager to clear the slot it was stored in. From c02e5ee8dcea6fe1797cb63869475013f24ab9e7 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:13:05 +0800 Subject: [PATCH 128/166] Stop PyBreeze's runs from Stop All Program too --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture.md | 1 + architecture_explore.md | 4 +-- docs/source/Eng/menu_file_run_text.rst | 3 +- docs/source/Zh/menu_file_run_text.rst | 4 +-- docs/updates/2026-09-c.md | 8 +++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/editor_main/main_ui.py | 13 ++++++++ test/test_utils/test_jeditor_contract.py | 5 +++ test/test_utils/test_stop_all_runs.py | 34 +++++++++++++++++++++ 12 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 test/test_utils/test_stop_all_runs.py diff --git a/README.md b/README.md index c034c0cd..3c8fe393 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w - **Automation keyword sets** — the `AT_*` / GUI / Web / Load keyword sets are registered for `.json`, and TestPioneer's schema for `.yml` and `.yaml`, on top of JEditor's language support. JEditor does not colour them yet: it highlights those files with its own rules for the suffix, which leave registered keywords out - **Code editor** — built on [JEditor](https://github.com/Integration-Automation/JEDITOR): tabs, project tree, format checker, debugger, terminal, variable inspector and a git client pane -- **Script execution** — single or batch, each run in a window of its own with a Stop button; action files are passed by path, and a script from the tab in front that is too long for a Windows command line (~32 KB) goes through a temporary file +- **Script execution** — single or batch, each run in a window of its own with a Stop button (Run ▸ Stop All Program stops them all); action files are passed by path, and a script from the tab in front that is too long for a Windows command line (~32 KB) goes through a temporary file - **Report generation** — HTML / JSON / XML after a run, with optional email delivery - **Integrated JupyterLab** — launches as a tab, in the same interpreter a run would use, installing JupyterLab there if it is missing - **Virtual environment awareness** — a run uses the interpreter chosen under **Python Env**; with none chosen, a `venv/` or `.venv/` in the working folder is detected and used, and without one, the interpreter the IDE runs on diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 3a4a060d..a756b884 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -74,7 +74,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: - **自动化关键字集** — 在 JEditor 的语言支持之上,为 `.json` 注册了 `AT_*` / GUI / Web / Load 关键字集,为 `.yml` 和 `.yaml` 注册了 TestPioneer 的结构定义。JEditor 目前还不会为它们着色:它用自己针对这些扩展名的规则高亮,不会用到注册的关键字 - **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建:标签页、项目树、格式检查、调试器、终端、变量查看器以及 git 客户端面板 -- **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮;动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 +- **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮(Run ▸ Stop All Program 会全部停止);动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 - **报告生成** — 运行后生成 HTML / JSON / XML 报告,可选以邮件发送 - **集成 JupyterLab** — 以标签页方式启动,使用与运行脚本相同的解释器;那里没有 JupyterLab 时会自动安装 - **虚拟环境感知** — 运行时使用在 **Python Env** 中选择的解释器;没有选择时,自动检测并使用工作文件夹中的 `venv/` 或 `.venv/`,都没有时则以 IDE 本身所用的解释器运行 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 46c4dc23..9dfe4866 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -74,7 +74,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: - **自動化關鍵字集** — 在 JEditor 的語言支援之上,`AT_*`/GUI/Web/Load 關鍵字集註冊給 `.json`,TestPioneer 的結構描述註冊給 `.yml` 與 `.yaml`。JEditor 目前還不會為它們上色:它用自己針對這些副檔名的規則高亮,不會用到註冊的關鍵字 - **程式碼編輯器** — 以 [JEditor](https://github.com/Integration-Automation/JEDITOR) 為基礎:分頁、專案樹、格式檢查、除錯器、終端機、變數檢視器與 git 用戶端面板 -- **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕;動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 +- **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕(Run ▸ Stop All Program 會全部停止);動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 - **報告產生** — 執行後產生 HTML/JSON/XML 報告,並可選擇以電子郵件寄送 - **整合 JupyterLab** — 以分頁方式啟動,使用與執行腳本相同的直譯器;那裡沒有 JupyterLab 時會自動安裝 - **虛擬環境感知** — 執行時使用在 **Python Env** 選擇的直譯器;沒有選擇時,自動偵測並使用工作資料夾中的 `venv/` 或 `.venv/`,都沒有時則以 IDE 本身使用的直譯器執行 diff --git a/architecture.md b/architecture.md index 53a4a1fe..0f6237e3 100644 --- a/architecture.md +++ b/architecture.md @@ -157,6 +157,7 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() back after `rename_self_tab()` clears it), on `EditorMain.close_tab(index)` (overridden to ask a tab's `may_close()` first, and to delete a closed tool tab, which its `removeTab` keeps) and on `CodeEditor`'s `reset_highlighter()`, `load_git_baseline()` and `start_language_server()` (a rename moves the tab the way `open_an_file` does), on `EditorMain`'s + `run_menu.stop_all_program_action` (Stop All Program also stops every run window's run), its `dock_menu` and its AI submenu `dock_ai_menu` (PyBreeze's AI docks join it; without one they get an AI submenu of their own, `menu/tools/tools_menu.py`), on the syntax highlighter taking a theme colour key (`warning_output_color`, `diff_modified_marker_color`, in both JEditor's dark and light diff --git a/architecture_explore.md b/architecture_explore.md index ef20d83a..a4ed8339 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -162,7 +162,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) - `output_queue()` — 每條管線的 queue 最多 `MAX_QUEUED_MESSAGES`(10,000)則;滿了 reader 就等(每 0.2 秒看一次 `keep_reading`),子行程寫管線也跟著等,跑得跟視窗顯示一樣快,像終端機;以前不設上限,印個不停的腳本會一直吃記憶體,按 Stop 後再一口氣全倒進視窗 - `ReaderGrace` / `any_alive()` — 子行程結束後 reader 還能讀多久(`READER_GRACE_SECONDS = 2.0`,從結束後第一個 tick 起算)。管線要等最後一個握著它的行程結束才會 EOF,子行程開的行程沒轉向輸出時會一直握著;以前兩個執行器在 UI 執行緒上各 join 2 秒,IDE 卡 4 秒還是丟掉之後的輸出。現在由 pump 逐 tick 詢問,時間到就結束執行並在視窗註明 - 執行器寫進執行視窗、說明這次執行本身的訊息(`[Error] Command not found: …`、`[Compile]`、`[Run]`、`[Stopped]`、`[Mail] …`、`Task exit with code …`、行程仍握著輸出的註明,共 15 種)一律經 `run_notice.run_notice(名稱, **欄位)`:取語言字典的 `run_window_<名稱>` 填入欄位,字典沒有時(腳本、測試在 `update_language_dict()` 之前啟動執行器)退回 PyBreeze 的英文;`test_run_notice.py` 擋掉在執行器裡直接寫 `"[Error] …"` 字串 -- 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。 +- 執行視窗上方有「停止」按鈕:執行器在子行程跑起來後呼叫 `CodeWindow.run_started()` 打開它,`run_ended()` 關掉它,按下去走 `stop_runner()`(`stop_tree` 停掉子行程和它開的所有行程);關掉執行視窗不會停止執行。Run > Stop All Program(JEditor 的 `run_menu.stop_all_program_action`,本來只停 JEditor 自己選單開的程式)另接到 `PyBreezeMainWindow.stop_all_runs()`,對每個執行視窗呼叫 `stop_runner()`,視窗與輸出留著。 - `CodeWindow.append_output(text, is_error, own_line=False)`(`show_code_window/code_window.py`,輸出是上限 10,000 行的 `QPlainTextEdit`:`QTextEdit` 到上限後每寫一行要花約 15 ms 丟掉最舊的一行)— 一律寫在文件**尾端**(不用 widget 自己的游標:那個游標跟著使用者的點擊與選取走,寫在那裡會把輸出插進中間、或蓋掉使用者選取的文字)。終端機控制碼(CSI 顏色與游標移動、OSC 等控制字串、`ESC ( B` 這類 nF 與其他兩位元組 escape)先拿掉、backspace 套用到前一個字元、tab、換行、`\r` 以外的控制字元丟掉(與 SSH terminal 共用 `utils/terminal_text.strip_terminal_controls()`;讀取切斷在 escape 中間時,reader 把尾巴留給下一段,`queue_pump` 的 `split_incomplete_escape()`),`\r\n` 是換行,單獨的 `\r` 像終端機一樣回到行首、由後面的文字取代這一行(`pybreeze_ui/terminal_view.insert_rewinding()`;結尾的 `\r` 記在 `_rewind_pending`,等下一段來才套用:接著是 `\n` 就是換行,否則回捲,跑完的進度條不會被清掉);輸出用等寬字型(`fixed_pitch.use_fixed_pitch_font()`:有 Consolas 用 Consolas,否則系統的等寬字型,Windows 上是 Courier New,與 SSH terminal 共用);換行只出現在文字本身有換行的地方,所以超過 buffer 被切段的長行會接回同一行。`own_line=True` 給視窗自己的狀態訊息(`Task exit with code …`),程式留下沒換行的半行時先補一個換行。捲軸在最底時畫面跟著輸出走(像終端機);使用者往上捲去讀時就停在原處 --- @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 144 個 `test_*.py`、2601 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2603 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/source/Eng/menu_file_run_text.rst b/docs/source/Eng/menu_file_run_text.rst index dcfc80fc..8fd1f2cc 100644 --- a/docs/source/Eng/menu_file_run_text.rst +++ b/docs/source/Eng/menu_file_run_text.rst @@ -70,7 +70,8 @@ Run Menu - Stops the tab's program, shell command and debugger. * - **Stop All Program** - Stops every program, shell command, debugger and ``pip`` run started from these - menus, in any tab. PyBreeze's own runs have their run window's **Stop** button. + menus, in any tab, and the run in every PyBreeze run window (automation scripts, + installs, **Run with...**); the run windows stay open with their output. * - **Run Help > Run Help** / **Shell Help** - Tips: check the interpreter, and match the encoding to the shell's. * - **Run with...** diff --git a/docs/source/Zh/menu_file_run_text.rst b/docs/source/Zh/menu_file_run_text.rst index 73fac4e2..4d914025 100644 --- a/docs/source/Zh/menu_file_run_text.rst +++ b/docs/source/Zh/menu_file_run_text.rst @@ -63,8 +63,8 @@ Run 選單 * - **Stop current program** - 停止分頁的程式、Shell 指令與除錯器。 * - **Stop All Program** - - 停止從這些選單啟動、在任何分頁中執行的程式、Shell 指令、除錯器與 ``pip``。 - PyBreeze 自己的執行由執行視窗的 **Stop** 按鈕停止。 + - 停止從這些選單啟動、在任何分頁中執行的程式、Shell 指令、除錯器與 ``pip``,以及每個 PyBreeze + 執行視窗中的執行(自動化腳本、安裝、**Run with...**);執行視窗與輸出會留著。 * - **Run Help > Run Help** / **Shell Help** - 提示:確認直譯器,並讓編碼與 Shell 的一致。 * - **Run with...** diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 2c983672..95edf74b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -649,3 +649,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst`, `docs/source/Eng/getting_started.rst`, `docs/source/Zh/getting_started.rst`, `progress.md` - **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning, and no built page shows a stray `**` or double backquote. The Run Debugger limit was reproduced in the real window: after `pdb` quit (exit 0) a second Run Debugger in the tab was refused. - **Open items**: `progress.md` #114, #115. + +## U-20260925-77 · 2026-09-25 · Stop All Program stops PyBreeze's runs too · #fix #run + +- **What**: Run > Stop All Program is JEditor's: it stops the programs, shell commands, debuggers and pip runs its own menus started. An automation script, a batch, a package install or a Run with... run shows in one of PyBreeze's run windows, and went on after it; only each window's Stop button, or closing the IDE, stopped them. Found through the subagent's reading of JEditor's base menus for the Sphinx File, Run, Text page. +- **Change**: `PyBreezeMainWindow.stop_all_runs()` calls `stop_runner()` on every run window, each guarded so one that fails is logged and the others still stop, and is connected to JEditor's `run_menu.stop_all_program_action` as the window is built. The windows stay open with their output. The three READMEs (Script execution), the Run page (both languages), `architecture_explore.md` and `architecture.md` §6 (the action PyBreeze relies on) say so, and `test_jeditor_contract.py` checks the action is still built. +- **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_stop_all_runs.py` (new), `test/test_utils/test_jeditor_contract.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`, `architecture_explore.md`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst` +- **Checks**: `test_stop_all_runs.py` starts the real window in a child, adds two run windows and one whose stop raises, and triggers Stop All Program: both are stopped and all three stay (failed before the change: none was stopped). The contract, IDE-closing and docs-markup tests pass (71); the Sphinx build gives no warning. `test/test_utils` collects 2603 tests in 145 files; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 5c5af10b..ca5d47fd 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-77 | 2026-09-25 | Stop All Program stops PyBreeze's runs too | #fix #run | [2026-09-c](2026-09-c.md) | | U-20260925-76 | 2026-09-25 | The Sphinx overview, getting-started and base-menu pages told right | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-75 | 2026-09-25 | start_editor's theme is the one the IDE shows | #fix #startup | [2026-09-c](2026-09-c.md) | | U-20260925-74 | 2026-09-25 | The main window opens from its own function | #refactor #startup | [2026-09-c](2026-09-c.md) | @@ -479,4 +480,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 76 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 77 | diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index 1d4d0351..332a5b4d 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -93,6 +93,8 @@ def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, # Menu add_menu_to_menubar(self) syntax_extend_package(self) + # JEditor's Stop All Program stops what its own menus started; PyBreeze's runs join it + self.run_menu.stop_all_program_action.triggered.connect(self.stop_all_runs) # Tab self._add_extend_tabs() @@ -151,6 +153,17 @@ def _tool_tabs_may_close(self) -> bool: dock.already_asked = True return agreed + def stop_all_runs(self) -> None: + """Stop the run in every run window: an automation script, a package install, a Run with... run. + + Connected to Run > Stop All Program, which stopped only the programs + JEditor's own menus started. The windows stay open with their output; + one whose stop fails is logged and the others are still stopped. + """ + # Over a copy: a window that ends its run may drop itself from the list + for run_window in tuple(self.current_run_code_window): + _close_guarded(run_window, run_window.stop_runner) + def closeEvent(self, event) -> None: # Asked before anything is stopped: a No keeps the IDE open as it was if not self._tool_tabs_may_close(): diff --git a/test/test_utils/test_jeditor_contract.py b/test/test_utils/test_jeditor_contract.py index 6467ef74..cbedc2c1 100644 --- a/test/test_utils/test_jeditor_contract.py +++ b/test/test_utils/test_jeditor_contract.py @@ -88,6 +88,11 @@ def test_the_main_window_takes_the_arguments_pybreeze_passes(self): assert callable(EditorMain.clear_code_result) assert callable(EditorMain.startup_setting) + def test_the_run_menu_keeps_its_stop_all_action(self): + # PyBreezeMainWindow connects its run windows' stop to this action. + build = importlib.import_module("je_editor.pyside_ui.main_ui.menu.run_menu.build_run_menu") + assert "run_menu.stop_all_program_action" in inspect.getsource(build) + def test_the_startup_applies_the_saved_theme(self): from je_editor import EditorMain diff --git a/test/test_utils/test_stop_all_runs.py b/test/test_utils/test_stop_all_runs.py new file mode 100644 index 00000000..9d6be2f7 --- /dev/null +++ b/test/test_utils/test_stop_all_runs.py @@ -0,0 +1,34 @@ +"""Run > Stop All Program stops PyBreeze's runs too, not only JEditor's. + +JEditor's Stop All Program stops the programs its own menus started. An +automation script, a package install or a Run with... run shows in a run window +of PyBreeze's, and went on after it. +""" +from __future__ import annotations + +from test_utils.started_window import run_started_window + +_BODY = """ +class FakeRunWindow: + stopped = 0 + + def stop_runner(self): + FakeRunWindow.stopped += 1 + + +class BrokenRunWindow: + def stop_runner(self): + raise RuntimeError("its process is gone") + + +window.current_run_code_window.extend([FakeRunWindow(), BrokenRunWindow(), FakeRunWindow()]) +window.run_menu.stop_all_program_action.trigger() +result = {"stopped": FakeRunWindow.stopped, "kept": len(window.current_run_code_window)} +# The fakes cannot close: the harness closes the window next +window.current_run_code_window.clear() +""" + + +def test_stop_all_program_stops_every_run_window(tmp_path): + # One whose stop fails costs only its own stop; the windows stay, with their output + assert run_started_window(tmp_path, _BODY) == {"stopped": 2, "kept": 3} From 09b022a0defbe6b39e26ebc6a66ff032cbe3e7e1 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:15:17 +0800 Subject: [PATCH 129/166] Say what a file tree click does to the tab in front --- docs/source/Eng/menu_file_run_text.rst | 3 ++- docs/source/Eng/ui_overview.rst | 11 ++++++++++- docs/source/Zh/menu_file_run_text.rst | 2 +- docs/source/Zh/ui_overview.rst | 9 ++++++++- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- progress.md | 1 + 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/source/Eng/menu_file_run_text.rst b/docs/source/Eng/menu_file_run_text.rst index 8fd1f2cc..cc376676 100644 --- a/docs/source/Eng/menu_file_run_text.rst +++ b/docs/source/Eng/menu_file_run_text.rst @@ -20,7 +20,8 @@ File Menu * - **New File** - Asks for a name and a place, and creates an empty file there (it is not opened). * - **Open File** - - Opens a file into the editor tab in front, replacing what it shows. + - Opens a file into the editor tab in front, replacing what it shows, without asking + about unsaved text there (see :doc:`ui_overview`). * - **Open Folder** - Makes a folder the working folder: the file tree shows it, and its settings are loaded (see :doc:`getting_started`). diff --git a/docs/source/Eng/ui_overview.rst b/docs/source/Eng/ui_overview.rst index a32f96df..6fd72bcb 100644 --- a/docs/source/Eng/ui_overview.rst +++ b/docs/source/Eng/ui_overview.rst @@ -42,11 +42,20 @@ Located on the left side of the window. Provides a file browser for navigating y directory. You can: - Browse files and folders -- Double-click to open files in the editor +- Click a file to open it in the editor tab in front, in place of what that tab shows + (unsaved text there is lost without a question, see the note below); a file already + open in a tab switches to that tab instead - Right-click any file or folder to access the context menu (see below) - Press **F2** to rename, or **Delete** to delete, the item in focus while the tree has the focus +.. note:: + + With JEditor 1.0.27, opening a file into a tab -- a click in the tree, or **File > Open + File** -- does not ask about the text that tab holds. A tab with a file saves it every + few seconds, but a new tab's text, or the last seconds of typing, is gone. Open a new + tab first (**Tab > Add Editor Tab**) to keep it. + File Tree Context Menu """""""""""""""""""""" diff --git a/docs/source/Zh/menu_file_run_text.rst b/docs/source/Zh/menu_file_run_text.rst index 4d914025..69cc857a 100644 --- a/docs/source/Zh/menu_file_run_text.rst +++ b/docs/source/Zh/menu_file_run_text.rst @@ -19,7 +19,7 @@ File 選單 * - **New File** - 詢問名稱與位置,在那裡建立空檔案(不會開啟它)。 * - **Open File** - - 把檔案開進前景的編輯分頁,取代它原本顯示的內容。 + - 把檔案開進前景的編輯分頁,取代它原本顯示的內容,不會詢問那裡未存的文字(見 :doc:`ui_overview`)。 * - **Open Folder** - 把一個資料夾設為工作資料夾:檔案樹顯示它,並載入它的設定(見 :doc:`getting_started`)。 * - **Save File** diff --git a/docs/source/Zh/ui_overview.rst b/docs/source/Zh/ui_overview.rst index d71fbcd0..abe7d091 100644 --- a/docs/source/Zh/ui_overview.rst +++ b/docs/source/Zh/ui_overview.rst @@ -37,10 +37,17 @@ PyBreeze 提供基於分頁和停靠面板的介面,使用 PySide6(Qt for Py 位於視窗左側,提供檔案瀏覽器用於導覽專案目錄。您可以: - 瀏覽檔案和資料夾 -- 雙擊在編輯器中開啟檔案 +- 單擊檔案會把它開進前景的編輯分頁,取代該分頁顯示的內容(那裡未存的文字會直接消失,不會詢問,見下方說明); + 已在某個分頁開啟的檔案則會切換到那個分頁 - 右鍵任何檔案或資料夾以開啟功能選單(詳見下方) - 檔案樹有焦點時,按 **F2** 重新命名、按 **Delete** 刪除焦點所在的項目 +.. note:: + + 在 JEditor 1.0.27 中,把檔案開進分頁——在檔案樹中單擊,或 **File > Open File**——不會詢問該分頁原本的文字。 + 有檔案的分頁每隔幾秒會自動存檔,但新分頁的文字、或最後幾秒打的字會消失。要保留它們,請先開新分頁 + (**Tab > Add Editor Tab**)。 + 檔案樹右鍵選單 """""""""""""" diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 95edf74b..14bce26f 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -657,3 +657,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_stop_all_runs.py` (new), `test/test_utils/test_jeditor_contract.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture.md`, `architecture_explore.md`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst` - **Checks**: `test_stop_all_runs.py` starts the real window in a child, adds two run windows and one whose stop raises, and triggers Stop All Program: both are stopped and all three stay (failed before the change: none was stopped). The contract, IDE-closing and docs-markup tests pass (71); the Sphinx build gives no warning. `test/test_utils` collects 2603 tests in 145 files; `ruff` clean. - **Open items**: none. + +## U-20260925-78 · 2026-09-25 · The Sphinx pages say what a tree click does to a tab's text · #docs + +- **What**: the UI Overview page (U-20260925-76 kept the line) said a double-click in the file tree opens a file. It is a single click, and it opens the file into the editor tab in front, replacing what that tab holds without asking, as File > Open File does (JEditor's `open_an_file`). Reproduced on je_editor 1.0.27 in the real window: an untitled tab's text was replaced and nothing was asked. A tab with a file loses only the last seconds of typing, since it is saved every 5 s. +- **Change**: UI Overview (both languages) says a click opens the file into the tab in front, and a file open in another tab switches to it; a note says unsaved text there is not asked about, and to open a new tab first to keep it. The File menu's Open File row says the same. `progress.md` #116 records it for JEditor. +- **Files**: `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst`, `progress.md` +- **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning. +- **Open items**: `progress.md` #116. diff --git a/docs/updates/README.md b/docs/updates/README.md index ca5d47fd..a997c6e5 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-78 | 2026-09-25 | The Sphinx pages say what a tree click does to a tab's text | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-77 | 2026-09-25 | Stop All Program stops PyBreeze's runs too | #fix #run | [2026-09-c](2026-09-c.md) | | U-20260925-76 | 2026-09-25 | The Sphinx overview, getting-started and base-menu pages told right | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-75 | 2026-09-25 | start_editor's theme is the one the IDE shows | #fix #startup | [2026-09-c](2026-09-c.md) | @@ -480,4 +481,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 77 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 78 | diff --git a/progress.md b/progress.md index fbfd33bd..09064931 100644 --- a/progress.md +++ b/progress.md @@ -24,3 +24,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#113** [BLOCKED] A translation plugin's language is listed in the Language menu under its key, not the name it registers: `register_natural_language(key, display_name, word_dict)` (`je_editor/plugins/__init__.py`) writes the dictionary into `language_wrapper.choose_language_dict` but not `display_names`, so `available_languages()` lists it and `display_name()` gives the key (`je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py:47-49`), and the loop over `get_all_natural_languages()` below skips it as already listed. Seen with the Sphinx Plugins page's French example ("French" shown, not "Français") on je_editor 1.0.27. Needs JEditor's `register_natural_language` to call `language_wrapper.register_language(key, word_dict, display_name)`. - **#114** [BLOCKED] File > Save File, Run Program and Run Debugger open a Save As dialog every time, starting in the working folder with no name filled in, even for a tab that already has a file: each calls `choose_file_get_save_file_path()` (`je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py:96`, `.../run_menu/under_run_menu/build_program_menu.py:95`, `build_debug_menu.py:106`), which always asks, on je_editor 1.0.27. PyBreeze's own Run with... saves in place (`save_current_file_for_run`). Needs JEditor to write a tab with a file where it is, and ask only for one without; the Sphinx File/Run/Text page says what it does now. - **#115** [BLOCKED] Run Debugger works once per editor tab: when the `pdb` run ends, `ExecManager.full_exit_program()` clears the tab's `exec_program`, not `exec_python_debugger` (`je_editor/pyside_ui/code/code_process/code_exec.py:228`), and nothing else clears it, not Stop current program (`build_run_menu.py:117`) or Stop All, so the next Run Debugger in that tab says a program is still running (`build_debug_menu.py:102`). Reproduced on je_editor 1.0.27 in the real window: after `pdb` quit (exit 0) the slot was still set and a second Run Debugger was refused. Needs the debugger's manager to clear the slot it was stored in. +- **#116** [BLOCKED] Opening a file into an editor tab replaces the text it holds without asking: a click in the project tree (`project_treeview.clicked` -> `treeview_click`) and File > Open File both call `EditorWidget.open_an_file()` (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:262`), which checks only whether the file is open elsewhere, then `setPlainText`s it. A tab with a file loses the last seconds of typing (auto-save runs every 5 s); a new tab loses everything. Reproduced on je_editor 1.0.27 in the real window: an untitled tab's text was replaced, nothing asked. Needs JEditor to ask when the tab has unsaved edits (its `_is_modified`), or open into a new tab; the Sphinx UI Overview says what happens now. From f4551c49ca3521fc71e913f629bdb8beedd14508 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:17:31 +0800 Subject: [PATCH 130/166] Decide why a key did not load in one place --- docs/updates/2026-09-c.md | 8 +++++++ docs/updates/README.md | 3 ++- .../connect_gui/ssh/ssh_key_loader.py | 24 ++++++++++++------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 14bce26f..f117f319 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -665,3 +665,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst`, `progress.md` - **Checks**: `test_docs_markup.py` passes (26); the Sphinx build gives no warning. - **Open items**: `progress.md` #116. + +## U-20260925-79 · 2026-09-25 · Why a key did not load, decided in one place · #refactor #ssh + +- **What**: refactor, no change in behaviour. `unloadable_key_reason` had a cognitive complexity of 17, over the gate of 15 in CLAUDE.md (measured with the `cognitive_complexity` package over every function in `pybreeze/`; it was the only one over), because the choice between no passphrase, a wrong one and an unsupported key was written out twice, for an encrypted PKCS#8 file and for a key a paramiko class asks a passphrase for. +- **Change**: `_asks_for_passphrase(key_path)` tries the key classes, and `_encrypted_key_reason(key_path, password)` makes the choice once; `unloadable_key_reason` scores 3, and no function in the module more than 7. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py` +- **Checks**: `test_ssh_security.py` (which calls `unloadable_key_reason` with real RSA, ECDSA, Ed25519, PKCS#8, PuTTY and DSA key files) and `test_ssh_login_form.py` pass unchanged (37), and `test_ssh_security.py` also with paramiko 5.0.0 (27); `ruff` clean. The other quality gates were checked at the same time: `ruff`'s complexity, argument, branch and nesting rules at the CLAUDE.md limits, no function over 60 lines of code, no file over 1000 lines. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index a997c6e5..8569bfc9 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-79 | 2026-09-25 | Why a key did not load, decided in one place | #refactor #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-78 | 2026-09-25 | The Sphinx pages say what a tree click does to a tab's text | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-77 | 2026-09-25 | Stop All Program stops PyBreeze's runs too | #fix #run | [2026-09-c](2026-09-c.md) | | U-20260925-76 | 2026-09-25 | The Sphinx overview, getting-started and base-menu pages told right | #docs | [2026-09-c](2026-09-c.md) | @@ -481,4 +482,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 78 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 79 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py index aea319f9..9b1dc5f3 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py @@ -105,20 +105,28 @@ def unloadable_key_reason(key_path: str, password: str) -> str: """ if _key_file_data(key_path, _PUTTY_HEADER) is not None: return PUTTY_KEY - if _key_file_data(key_path, _PKCS8_ENCRYPTED) is not None: - if not password: - return PASSPHRASE_NEEDED - return UNSUPPORTED_KEY if _decrypts(key_path, password) else PASSPHRASE_WRONG + if _key_file_data(key_path, _PKCS8_ENCRYPTED) is not None or _asks_for_passphrase(key_path): + return _encrypted_key_reason(key_path, password) + return UNSUPPORTED_KEY + + +def _asks_for_passphrase(key_path: str) -> bool: + """Whether some key class asks for a passphrase to load *key_path*: the file is encrypted.""" for key_cls in _KEY_CLASSES: try: key_cls.from_private_key_file(key_path, None) except paramiko.PasswordRequiredException: - if not password: - return PASSPHRASE_NEEDED - return UNSUPPORTED_KEY if _decrypts(key_path, password) else PASSPHRASE_WRONG + return True except (paramiko.SSHException, ValueError, OSError) as error: pybreeze_logger.debug("Key type %s rejected: %s", key_cls.__name__, error) - return UNSUPPORTED_KEY + return False + + +def _encrypted_key_reason(key_path: str, password: str) -> str: + """Why an encrypted key file did not load: no passphrase, a wrong one, or a type paramiko cannot use.""" + if not password: + return PASSPHRASE_NEEDED + return UNSUPPORTED_KEY if _decrypts(key_path, password) else PASSPHRASE_WRONG def _decrypts(key_path: str, password: str) -> bool: From 7a7e167c01028dd0b178aa6945fb3f68c27df63c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:30:43 +0800 Subject: [PATCH 131/166] Stop offering the empty variable inspector as a feature --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/source/Eng/menu_file_run_text.rst | 3 ++- docs/source/Eng/ui_overview.rst | 3 ++- docs/source/Zh/menu_file_run_text.rst | 2 +- docs/source/Zh/ui_overview.rst | 2 +- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- progress.md | 1 + 10 files changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3c8fe393..c2e60c1a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Each module gets the same menu shape: **Run** (single script, batch directory, w ### IDE core - **Automation keyword sets** — the `AT_*` / GUI / Web / Load keyword sets are registered for `.json`, and TestPioneer's schema for `.yml` and `.yaml`, on top of JEditor's language support. JEditor does not colour them yet: it highlights those files with its own rules for the suffix, which leave registered keywords out -- **Code editor** — built on [JEditor](https://github.com/Integration-Automation/JEDITOR): tabs, project tree, format checker, debugger, terminal, variable inspector and a git client pane +- **Code editor** — built on [JEditor](https://github.com/Integration-Automation/JEDITOR): tabs, project tree, format checker, debugger, terminal and a git client pane - **Script execution** — single or batch, each run in a window of its own with a Stop button (Run ▸ Stop All Program stops them all); action files are passed by path, and a script from the tab in front that is too long for a Windows command line (~32 KB) goes through a temporary file - **Report generation** — HTML / JSON / XML after a run, with optional email delivery - **Integrated JupyterLab** — launches as a tab, in the same interpreter a run would use, installing JupyterLab there if it is missing diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index a756b884..faf2ec7c 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -73,7 +73,7 @@ PyBreeze 开箱即用,涵盖自动化测试的完整范围: ### IDE 核心 - **自动化关键字集** — 在 JEditor 的语言支持之上,为 `.json` 注册了 `AT_*` / GUI / Web / Load 关键字集,为 `.yml` 和 `.yaml` 注册了 TestPioneer 的结构定义。JEditor 目前还不会为它们着色:它用自己针对这些扩展名的规则高亮,不会用到注册的关键字 -- **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建:标签页、项目树、格式检查、调试器、终端、变量查看器以及 git 客户端面板 +- **代码编辑器** — 基于 [JEditor](https://github.com/Integration-Automation/JEDITOR) 构建:标签页、项目树、格式检查、调试器、终端以及 git 客户端面板 - **脚本执行** — 单个或批量运行,每次运行都有自己的窗口和 Stop 按钮(Run ▸ Stop All Program 会全部停止);动作文件以路径传入,而前台标签页中的脚本若超出 Windows 命令行的长度上限(约 32 KB),会改经临时文件传递 - **报告生成** — 运行后生成 HTML / JSON / XML 报告,可选以邮件发送 - **集成 JupyterLab** — 以标签页方式启动,使用与运行脚本相同的解释器;那里没有 JupyterLab 时会自动安装 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 9dfe4866..d6e671e8 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -73,7 +73,7 @@ PyBreeze 開箱即用,涵蓋自動化測試的完整範疇: ### IDE 核心 - **自動化關鍵字集** — 在 JEditor 的語言支援之上,`AT_*`/GUI/Web/Load 關鍵字集註冊給 `.json`,TestPioneer 的結構描述註冊給 `.yml` 與 `.yaml`。JEditor 目前還不會為它們上色:它用自己針對這些副檔名的規則高亮,不會用到註冊的關鍵字 -- **程式碼編輯器** — 以 [JEditor](https://github.com/Integration-Automation/JEDITOR) 為基礎:分頁、專案樹、格式檢查、除錯器、終端機、變數檢視器與 git 用戶端面板 +- **程式碼編輯器** — 以 [JEditor](https://github.com/Integration-Automation/JEDITOR) 為基礎:分頁、專案樹、格式檢查、除錯器、終端機與 git 用戶端面板 - **腳本執行** — 單一或批次執行,每次執行都有自己的視窗與 Stop 按鈕(Run ▸ Stop All Program 會全部停止);動作檔以路徑傳入,而目前分頁中的腳本若超過 Windows 命令列長度上限(約 32 KB),會改用暫存檔傳遞 - **報告產生** — 執行後產生 HTML/JSON/XML 報告,並可選擇以電子郵件寄送 - **整合 JupyterLab** — 以分頁方式啟動,使用與執行腳本相同的直譯器;那裡沒有 JupyterLab 時會自動安裝 diff --git a/docs/source/Eng/menu_file_run_text.rst b/docs/source/Eng/menu_file_run_text.rst index cc376676..e864a087 100644 --- a/docs/source/Eng/menu_file_run_text.rst +++ b/docs/source/Eng/menu_file_run_text.rst @@ -167,7 +167,8 @@ Tab and Dock Menus - **Toggle Split View** (the same document twice) and **Toggle Minimap**, for the tab in front - **Snippet Editor** -- the snippets in ``.jeditor/snippets.json`` -- **Tools Tab** -- IPython (Jupyter, inside the IDE's own Python), the variable inspector, +- **Tools Tab** -- IPython (Jupyter, inside the IDE's own Python), the variable inspector + (empty with JEditor 1.0.27: nothing gives it variables to show), FrontEngine, ChatUI, the TODO panel, the outline of the current Python file, and JupyterLab (see :doc:`jupyter_lab`) - **Git Tab** -- the Git client, the branch tree viewer, the code diff viewer, and the diff --git a/docs/source/Eng/ui_overview.rst b/docs/source/Eng/ui_overview.rst index 6fd72bcb..0ad23c64 100644 --- a/docs/source/Eng/ui_overview.rst +++ b/docs/source/Eng/ui_overview.rst @@ -137,7 +137,8 @@ Dock Widgets Panels can be opened as **dock widgets** instead of tabs from the **Dock** menu, and arranged freely around the main window: -- **Editor** -- a docked editor, IPython (Jupyter) and the variable inspector +- **Editor** -- a docked editor, IPython (Jupyter) and the variable inspector (empty for now, + see :doc:`menu_file_run_text`) - **Git** -- the Git client, branch tree viewer and code diff viewer - **AI** -- Chat UI, AI Code Review, CoT Prompt Editor, CoT Code Review, Skill Prompt Editor and Skill Send diff --git a/docs/source/Zh/menu_file_run_text.rst b/docs/source/Zh/menu_file_run_text.rst index 69cc857a..e1d6176e 100644 --- a/docs/source/Zh/menu_file_run_text.rst +++ b/docs/source/Zh/menu_file_run_text.rst @@ -149,7 +149,7 @@ Tab 與 Dock 選單 - **Console Widget** -- 互動式 Shell(``cmd``、PowerShell、``bash`` 或 ``sh``) - **Toggle Split View**\ (同一份文件顯示兩次)與 **Toggle Minimap**,作用在前景分頁 - **Snippet Editor** -- ``.jeditor/snippets.json`` 中的程式碼片段 -- **Tools Tab** -- IPython(Jupyter,在 IDE 自己的 Python 中)、變數檢視器、FrontEngine、ChatUI、TODO 面板、 +- **Tools Tab** -- IPython(Jupyter,在 IDE 自己的 Python 中)、變數檢視器(JEditor 1.0.27 中是空的:沒有東西提供變數給它)、FrontEngine、ChatUI、TODO 面板、 目前 Python 檔的大綱,以及 JupyterLab(見 :doc:`jupyter_lab`) - **Git Tab** -- Git 用戶端、分支樹檢視器、程式碼差異檢視器,以及目前檔案與 ``HEAD`` 或暫存版本的差異 - **Dock > Tools** 另有 **Problems**\ (``ruff`` 找到的問題)與 **Tests**\ (在工作資料夾執行 ``pytest``) diff --git a/docs/source/Zh/ui_overview.rst b/docs/source/Zh/ui_overview.rst index abe7d091..67a5076c 100644 --- a/docs/source/Zh/ui_overview.rst +++ b/docs/source/Zh/ui_overview.rst @@ -120,7 +120,7 @@ PyBreeze 提供基於分頁和停靠面板的介面,使用 PySide6(Qt for Py 面板可以從 **Dock** 選單以 **停靠面板** 開啟,而不是分頁,並可在主視窗周圍自由排列: -- **Editor** -- 停靠式編輯器、IPython(Jupyter)與變數檢視器 +- **Editor** -- 停靠式編輯器、IPython(Jupyter)與變數檢視器(目前是空的,見 :doc:`menu_file_run_text`) - **Git** -- Git 用戶端、分支樹檢視器與程式碼差異檢視器 - **AI** -- Chat UI、AI Code Review、CoT Prompt Editor、CoT Code Review、Skill Prompt Editor 與 Skill Send - **Tools** -- 瀏覽器、FrontEngine、主控台、TODO 面板、Problems、Tests 與大綱 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index f117f319..c75eb4f6 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -673,3 +673,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py` - **Checks**: `test_ssh_security.py` (which calls `unloadable_key_reason` with real RSA, ECDSA, Ed25519, PKCS#8, PuTTY and DSA key files) and `test_ssh_login_form.py` pass unchanged (37), and `test_ssh_security.py` also with paramiko 5.0.0 (27); `ruff` clean. The other quality gates were checked at the same time: `ruff`'s complexity, argument, branch and nesting rules at the CLAUDE.md limits, no function over 60 lines of code, no file over 1000 lines. - **Open items**: none. + +## U-20260925-80 · 2026-09-25 · The variable inspector is not offered as a feature · #docs + +- **What**: the three READMEs listed a variable inspector among the editor's features. JEditor's Variable Inspector (Tab > Tools Tab, Dock > Editor, and a tab of each editor's bottom panel) shows the namespace it is given, and nothing gives it one: it is always empty on je_editor 1.0.27. Found checking a subagent's reading of JEditor's base menus. +- **Change**: the READMEs' Code editor bullet no longer names it; the Sphinx File, Run, Text and UI Overview pages (both languages) say it is empty; `progress.md` #117 records it for JEditor. +- **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst`, `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst`, `progress.md` +- **Checks**: no code in je_editor 1.0.27 sets `VariableModel.namespace`; `test_docs_markup.py` passes (26) and the Sphinx build gives no warning. +- **Open items**: `progress.md` #117. diff --git a/docs/updates/README.md b/docs/updates/README.md index 8569bfc9..959b783b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-80 | 2026-09-25 | The variable inspector is not offered as a feature | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-79 | 2026-09-25 | Why a key did not load, decided in one place | #refactor #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-78 | 2026-09-25 | The Sphinx pages say what a tree click does to a tab's text | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-77 | 2026-09-25 | Stop All Program stops PyBreeze's runs too | #fix #run | [2026-09-c](2026-09-c.md) | @@ -482,4 +483,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 79 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 80 | diff --git a/progress.md b/progress.md index 09064931..85204c90 100644 --- a/progress.md +++ b/progress.md @@ -25,3 +25,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#114** [BLOCKED] File > Save File, Run Program and Run Debugger open a Save As dialog every time, starting in the working folder with no name filled in, even for a tab that already has a file: each calls `choose_file_get_save_file_path()` (`je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py:96`, `.../run_menu/under_run_menu/build_program_menu.py:95`, `build_debug_menu.py:106`), which always asks, on je_editor 1.0.27. PyBreeze's own Run with... saves in place (`save_current_file_for_run`). Needs JEditor to write a tab with a file where it is, and ask only for one without; the Sphinx File/Run/Text page says what it does now. - **#115** [BLOCKED] Run Debugger works once per editor tab: when the `pdb` run ends, `ExecManager.full_exit_program()` clears the tab's `exec_program`, not `exec_python_debugger` (`je_editor/pyside_ui/code/code_process/code_exec.py:228`), and nothing else clears it, not Stop current program (`build_run_menu.py:117`) or Stop All, so the next Run Debugger in that tab says a program is still running (`build_debug_menu.py:102`). Reproduced on je_editor 1.0.27 in the real window: after `pdb` quit (exit 0) the slot was still set and a second Run Debugger was refused. Needs the debugger's manager to clear the slot it was stored in. - **#116** [BLOCKED] Opening a file into an editor tab replaces the text it holds without asking: a click in the project tree (`project_treeview.clicked` -> `treeview_click`) and File > Open File both call `EditorWidget.open_an_file()` (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:262`), which checks only whether the file is open elsewhere, then `setPlainText`s it. A tab with a file loses the last seconds of typing (auto-save runs every 5 s); a new tab loses everything. Reproduced on je_editor 1.0.27 in the real window: an untitled tab's text was replaced, nothing asked. Needs JEditor to ask when the tab has unsaved edits (its `_is_modified`), or open into a new tab; the Sphinx UI Overview says what happens now. +- **#117** [BLOCKED] JEditor's Variable Inspector (Tab > Tools Tab, Dock > Editor, and each editor tab's bottom panel) is always empty: `VariableModel` shows the `namespace` it is given, and nothing gives it one (`je_editor/pyside_ui/code/variable_inspector/inspector_gui.py:17`, built with none at `editor_widget.py:151` and `build_tab_tools_menu.py:103`; no code sets `.namespace`), on je_editor 1.0.27. The README no longer lists it among the editor's features; the Sphinx pages say it is empty. Needs JEditor to feed it from somewhere (the IPython console's namespace, or a paused debugger's), or to drop it. From 47bd9f5a329036d81420143f8246aaa8cac51758 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:31:27 +0800 Subject: [PATCH 132/166] Re-measure the line counts architecture_explore.md quotes --- architecture_explore.md | 6 +++--- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index a4ed8339..616ee1c6 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -1,6 +1,6 @@ # PyBreeze 架構探勘 / Architecture Exploration -> 掃描範圍:`pybreeze/`(209 個 `.py`、約 24,000 行,不含空行與註解約 19,400 行)+ `test/`、`exe/`、`docs/`、CI 設定 +> 掃描範圍:`pybreeze/`(209 個 `.py`、約 24,200 行,不含空行與註解約 18,900 行)+ `test/`、`exe/`、`docs/`、CI 設定 > 對應版本:`pyproject.toml` 1.0.21(stable)/`dev.toml` 1.0.14(dev),分支 `dev` --- @@ -243,7 +243,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,034 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,042 行,最大子系統) | 檔案 | 職責 | |---|---| @@ -298,7 +298,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 9. `pybreeze_ui/connect_gui/` -### `ssh/`(2,508 行) +### `ssh/`(2,516 行) | 檔案 | 職責 | |---|---| diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index c75eb4f6..f8b222ee 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -681,3 +681,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `docs/source/Eng/menu_file_run_text.rst`, `docs/source/Zh/menu_file_run_text.rst`, `docs/source/Eng/ui_overview.rst`, `docs/source/Zh/ui_overview.rst`, `progress.md` - **Checks**: no code in je_editor 1.0.27 sets `VariableModel.namespace`; `test_docs_markup.py` passes (26) and the Sphinx build gives no warning. - **Open items**: `progress.md` #117. + +## U-20260925-81 · 2026-09-25 · architecture_explore.md's line counts re-measured · #docs + +- **What**: CLAUDE.md asks for the line counts `architecture_explore.md` quotes to be re-measured when the code they count changes. Today's changes to the diagram editor and the SSH key loader left two of them behind, and the header's count of lines without blanks or comments was 19,400 against 18,876 measured. +- **Change**: the header says about 24,200 lines, about 18,900 without blank lines and `#` comments (209 files, unchanged); the diagram editor section 4,042 lines (was 4,034); the `ssh/` section 2,516 (was 2,508). `syntax_keyword.py`'s 629 is still right. +- **Files**: `architecture_explore.md` +- **Checks**: counted with `wc -l` and a line filter over `pybreeze/**/*.py`. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 959b783b..0d126c07 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-81 | 2026-09-25 | architecture_explore.md's line counts re-measured | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-80 | 2026-09-25 | The variable inspector is not offered as a feature | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-79 | 2026-09-25 | Why a key did not load, decided in one place | #refactor #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-78 | 2026-09-25 | The Sphinx pages say what a tree click does to a tab's text | #docs | [2026-09-c](2026-09-c.md) | @@ -483,4 +484,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 80 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 81 | From 7e0d4e8158fa3a7698ed60bc1152d90589fff7aa Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:35:35 +0800 Subject: [PATCH 133/166] Translate a plugin's About box --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- architecture_explore.md | 6 +++--- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- pybreeze/extend_multi_language/extend_english.py | 1 + .../extend_traditional_chinese.py | 1 + .../menu/plugin_menu/build_plugin_menu.py | 6 +++--- test/test_utils/test_plugin_menu.py | 15 +++++++++++++++ 10 files changed, 36 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c2e60c1a..ec605954 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ Loaded plugins appear under their own **Plugins** menu with an About entry and o - **English** (default) - **Traditional Chinese** (繁體中文) -Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 759 keys, and a test enforces that parity so a new string can never land in one language only. The Language menu also lists JEditor's Japanese and Simplified Chinese: picked, JEditor's own menus change and PyBreeze's strings stay in English. Further languages can be added via translation plugins. +Menus, dialogs, the reasons a tool refuses its input and the run window's own notices (`[Error] …`, `[Run] …`) all follow the chosen language. Both dictionaries carry the same 760 keys, and a test enforces that parity so a new string can never land in one language only. The Language menu also lists JEditor's Japanese and Simplified Chinese: picked, JEditor's own menus change and PyBreeze's strings stay in English. Further languages can be added via translation plugins. --- diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index faf2ec7c..0b0d893c 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -215,7 +215,7 @@ PyBreeze 继承了 JEditor 的插件架构,会从工作目录下的 `jeditor_p - **English**(默认) - **繁體中文**(Traditional Chinese) -菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 759 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。语言菜单还列出 JEditor 的日文与简体中文:选择后 JEditor 自己的菜单会随之改变,PyBreeze 的字符串则保持英文。其他语言可以通过翻译插件添加。 +菜单、对话框、工具拒绝输入时给出的原因,以及运行窗口自身的提示(`[Error] …`、`[Run] …`)都会跟随所选语言。两份词典包含同样的 760 个键,并有测试确保两者一致,因此新字符串绝不会只出现在一种语言中。语言菜单还列出 JEditor 的日文与简体中文:选择后 JEditor 自己的菜单会随之改变,PyBreeze 的字符串则保持英文。其他语言可以通过翻译插件添加。 --- diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index d6e671e8..6845da54 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -215,7 +215,7 @@ PyBreeze 沿用 JEditor 的外掛架構,會自動從工作目錄中的 `jedito - **English**(英文,預設) - **繁體中文**(Traditional Chinese) -選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 759 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。語言選單另外列出 JEditor 的日文與簡體中文:選了之後 JEditor 自己的選單會改變,PyBreeze 的字串則維持英文。其他語言可透過翻譯外掛加入。 +選單、對話框、工具拒絕輸入時說明的原因,以及執行視窗自己的訊息(`[錯誤] …`、`[執行] …`)都會跟著所選的語言顯示。兩份字典都有相同的 760 個鍵,並有測試強制兩者一致,因此新字串不可能只出現在其中一種語言。語言選單另外列出 JEditor 的日文與簡體中文:選了之後 JEditor 自己的選單會改變,PyBreeze 的字串則維持英文。其他語言可透過翻譯外掛加入。 --- diff --git a/architecture_explore.md b/architecture_explore.md index 616ee1c6..a27cb2e9 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -66,7 +66,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - - `update_language_dict()` 併入 PyBreeze 的 759 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) + - `update_language_dict()` 併入 PyBreeze 的 760 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - `show_only_warnings_in_code_result()`(`pybreeze_ui/code_result_logs.py`)— JEditor 剛把一個 `RedirectStdErr` 掛到當下每個 logger 上、收到的顯示在 Code Result;自動化套件 import 時把 root 設成 DEBUG,所以開檔就有 gitpython 的除錯訊息以紅字出現。把這個 handler 的門檻調到 WARNING,logger 本身的層級不動 - 刪掉 JEditor 原本的 Help 選單 @@ -369,7 +369,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 13. `pybreeze/extend_multi_language/` -`extend_english.py` 與 `extend_traditional_chinese.py` 各 759 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 +`extend_english.py` 與 `extend_traditional_chinese.py` 各 760 個鍵,`update_language_dict()` 把它們併進 `je_editor` 的字典,並把 `application_name`(「PyBreeze」)寫進 `language_wrapper.choose_language_dict` 裡每一個語言:這是 PyBreeze 唯一覆寫而非新增的 JEditor 鍵,日文、簡中等 PyBreeze 沒翻譯的語言自帶「JEditor」,不寫的話會蓋過英文退回值。`test_language_parity.py` 守住兩邊鍵值必須對齊,也檢查每個已註冊語言都解得出程式用到的每個鍵;`test_startup_language.py` 在子行程裡用存好的繁中/日文真的啟動主視窗。 --- @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2603 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2604 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index f8b222ee..de306443 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -689,3 +689,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `architecture_explore.md` - **Checks**: counted with `wc -l` and a line filter over `pybreeze/**/*.py`. - **Open items**: none. + +## U-20260925-82 · 2026-09-25 · A plugin's About box speaks the IDE language · #fix #i18n #plugins + +- **What**: the About box of a loaded plugin (Plugins > the plugin, or its submenu's About) said "Version:" and "Author:" in English whatever the IDE spoke; it was the one piece of UI text in `pybreeze/` built outside the dictionaries (a search for literal English in labels, buttons, titles and message texts found no other). +- **Change**: the text comes from the key `plugin_about_text` (the name, then `Version: {version}` and `Author: {author}` on lines of their own; 版本 and 作者 in Traditional Chinese), still shown as plain text. The dictionaries have 760 keys each; the three READMEs and `architecture_explore.md` say so. +- **Files**: `pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_plugin_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` +- **Checks**: `test_the_about_dialog_speaks_the_ide_language` (failed before the change: the text was English in the Chinese IDE); the plugin menu, language parity, message box, keys-used and answered-box tests pass (77), and the README key-count tests. `test/test_utils` collects 2604 tests; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0d126c07..4444a346 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-82 | 2026-09-25 | A plugin's About box speaks the IDE language | #fix #i18n #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-81 | 2026-09-25 | architecture_explore.md's line counts re-measured | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-80 | 2026-09-25 | The variable inspector is not offered as a feature | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-79 | 2026-09-25 | Why a key did not load, decided in one place | #refactor #ssh | [2026-09-c](2026-09-c.md) | @@ -484,4 +485,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 81 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 82 | diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 681e16b5..68e98c22 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -399,6 +399,7 @@ # Plugin Menu "plugin_menu_label": "Plugins", "plugin_menu_about": "About", + "plugin_about_text": "{name}\nVersion: {version}\nAuthor: {author}", "plugin_menu_run_with": "Run with {name}", # Run with Menu "run_folder_title": "Run a folder", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index d691eb5f..eda5baab 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -766,6 +766,7 @@ # Plugin Menu "plugin_menu_label": "外掛", "plugin_menu_about": "關於", + "plugin_about_text": "{name}\n版本:{version}\n作者:{author}", "plugin_menu_run_with": "以 {name} 執行", # Run with Menu "run_folder_title": "執行資料夾", diff --git a/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py b/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py index a35ceb84..0ae459d5 100644 --- a/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py +++ b/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py @@ -135,9 +135,9 @@ def callback(): message_box.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) message_box.setWindowTitle(name) message_box.setText(as_text( - f"{name}\n" - f"Version: {version}\n" - f"Author: {author}" + language_wrapper.language_word_dict.get( + "plugin_about_text", "{name}\nVersion: {version}\nAuthor: {author}", + ).format(name=name, version=version, author=author) )) message_box.exec() return callback diff --git a/test/test_utils/test_plugin_menu.py b/test/test_utils/test_plugin_menu.py index 5d43d4cd..9e77aa7d 100644 --- a/test/test_utils/test_plugin_menu.py +++ b/test/test_utils/test_plugin_menu.py @@ -279,6 +279,21 @@ def test_the_about_dialog_names_version_and_author(self, app, monkeypatch): assert "2.1" in shown[0] assert "someone" in shown[0] + def test_the_about_dialog_speaks_the_ide_language(self, app, monkeypatch): + # "Version:" and "Author:" were English whatever the IDE spoke + from pybreeze.extend_multi_language.extend_traditional_chinese import ( + pybreeze_traditional_chinese_word_dict, + ) + monkeypatch.setattr(plugin_menu.language_wrapper, "language_word_dict", + pybreeze_traditional_chinese_word_dict) + shown: list[str] = [] + monkeypatch.setattr( + plugin_menu.QMessageBox, "exec", lambda self: shown.append(self.text())) + plugin_menu._make_about_callback(None, "Go", "2.1", "someone")() + + assert "版本" in shown[0] and "作者" in shown[0] + assert "Version" not in shown[0] and "Author" not in shown[0] + def test_the_about_dialog_shows_markup_as_text(self, app, monkeypatch): # A plugin's name or author went to the box as markup, and all from PySide6.QtGui import QTextDocument From ddced5ef9e2500042825619d7575cf4d59994c3a Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:42:30 +0800 Subject: [PATCH 134/166] Record that a damaged saved theme stops JEditor's window --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- progress.md | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index de306443..128e97e9 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -697,3 +697,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `test/test_utils/test_plugin_menu.py`, `README.md`, `README/README_zh-TW.md`, `README/README_zh-CN.md`, `architecture_explore.md` - **Checks**: `test_the_about_dialog_speaks_the_ide_language` (failed before the change: the text was English in the Chinese IDE); the plugin menu, language parity, message box, keys-used and answered-box tests pass (77), and the README key-count tests. `test/test_utils` collects 2604 tests; `ruff` clean. - **Open items**: none. + +## U-20260925-83 · 2026-09-25 · A damaged saved theme stops JEditor's window: recorded · #investigation + +- **What**: reviewing the theme change (U-20260925-75), I checked what a settings file with a `ui_style` that is not a theme name does. qt_material logs a warning for a name it does not know and raises `TypeError` for a number or `null`. With `{"ui_style": 42}` saved, the IDE did not start, before or after that change: JEditor's `EditorMain.__init__` calls `startup_setting()` itself, unguarded, and the error leaves the window's constructor before PyBreeze runs any code of its own. A guard in `open_main_window()` was tried and dropped, since it cannot be reached. +- **Change**: `progress.md` #118 records it for JEditor. No code change. +- **Files**: `progress.md` +- **Checks**: reproduced with a script that saves `{"ui_style": 42}` in a scratch working folder and builds the window: `TypeError` from `qt_material.get_theme` inside `EditorMain.__init__` (`main_editor.py:245`). +- **Open items**: `progress.md` #118. diff --git a/docs/updates/README.md b/docs/updates/README.md index 4444a346..d380d896 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-83 | 2026-09-25 | A damaged saved theme stops JEditor's window: recorded | #investigation | [2026-09-c](2026-09-c.md) | | U-20260925-82 | 2026-09-25 | A plugin's About box speaks the IDE language | #fix #i18n #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-81 | 2026-09-25 | architecture_explore.md's line counts re-measured | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-80 | 2026-09-25 | The variable inspector is not offered as a feature | #docs | [2026-09-c](2026-09-c.md) | @@ -485,4 +486,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 82 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 83 | diff --git a/progress.md b/progress.md index 85204c90..31e044be 100644 --- a/progress.md +++ b/progress.md @@ -26,3 +26,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#115** [BLOCKED] Run Debugger works once per editor tab: when the `pdb` run ends, `ExecManager.full_exit_program()` clears the tab's `exec_program`, not `exec_python_debugger` (`je_editor/pyside_ui/code/code_process/code_exec.py:228`), and nothing else clears it, not Stop current program (`build_run_menu.py:117`) or Stop All, so the next Run Debugger in that tab says a program is still running (`build_debug_menu.py:102`). Reproduced on je_editor 1.0.27 in the real window: after `pdb` quit (exit 0) the slot was still set and a second Run Debugger was refused. Needs the debugger's manager to clear the slot it was stored in. - **#116** [BLOCKED] Opening a file into an editor tab replaces the text it holds without asking: a click in the project tree (`project_treeview.clicked` -> `treeview_click`) and File > Open File both call `EditorWidget.open_an_file()` (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:262`), which checks only whether the file is open elsewhere, then `setPlainText`s it. A tab with a file loses the last seconds of typing (auto-save runs every 5 s); a new tab loses everything. Reproduced on je_editor 1.0.27 in the real window: an untitled tab's text was replaced, nothing asked. Needs JEditor to ask when the tab has unsaved edits (its `_is_modified`), or open into a new tab; the Sphinx UI Overview says what happens now. - **#117** [BLOCKED] JEditor's Variable Inspector (Tab > Tools Tab, Dock > Editor, and each editor tab's bottom panel) is always empty: `VariableModel` shows the `namespace` it is given, and nothing gives it one (`je_editor/pyside_ui/code/variable_inspector/inspector_gui.py:17`, built with none at `editor_widget.py:151` and `build_tab_tools_menu.py:103`; no code sets `.namespace`), on je_editor 1.0.27. The README no longer lists it among the editor's features; the Sphinx pages say it is empty. Needs JEditor to feed it from somewhere (the IPython console's namespace, or a paused debugger's), or to drop it. +- **#118** [BLOCKED] A settings file whose `ui_style` is not a string stops the IDE from starting: `EditorMain.__init__` calls `startup_setting()` unguarded (`je_editor/pyside_ui/main_ui/main_editor.py:245`), which hands the value to qt_material's `apply_stylesheet`, and a number raises `TypeError` inside the window's constructor, before PyBreeze can do anything (reproduced on je_editor 1.0.27 with `{"ui_style": 42}` in `.jeditor/user_setting.json`: the window was never built). A theme name that does not exist only logs a warning. Needs JEditor to check the saved style's type, or to guard that call as `_restore_open_files_session` is guarded. From 177129956a70cab5195e78f56bd39e93308d10c3 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:44:12 +0800 Subject: [PATCH 135/166] Test that a closed run window's run can still be stopped --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++++ docs/updates/README.md | 3 ++- progress.md | 2 +- test/test_utils/test_stop_all_runs.py | 35 +++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index a27cb2e9..3cc1fc5a 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2604 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2605 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 128e97e9..0f026ed9 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -705,3 +705,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `progress.md` - **Checks**: reproduced with a script that saves `{"ui_style": 42}` in a scratch working folder and builds the window: `TypeError` from `qt_material.get_theme` inside `EditorMain.__init__` (`main_editor.py:245`). - **Open items**: `progress.md` #118. + +## U-20260925-84 · 2026-09-25 · A run whose window was closed can be stopped from the Run menu · #test #run + +- **What**: `progress.md` #2 said a run whose window had been closed could be stopped only by a task manager or by closing the IDE. A window closed while its run goes on stays in the main window's list (`CodeWindow.closeEvent`), so Stop All Program, which since U-20260925-77 stops every run window's run, reaches it too; nothing tested that. +- **Change**: `test_a_run_whose_window_was_closed_can_still_be_stopped` opens a real run window with `open_run_window`, gives it a runner whose process is still running, closes the window and triggers Stop All Program: the window was kept and its run stopped. #2 now says Stop All Program or closing the IDE stops such a run; whether closing one window should stop its run is still the owner's decision. +- **Files**: `test/test_utils/test_stop_all_runs.py`, `progress.md`, `architecture_explore.md` (test count) +- **Checks**: `test_stop_all_runs.py` passes (2); `test/test_utils` collects 2605 tests; `ruff` clean. +- **Open items**: `progress.md` #2 (the decision). diff --git a/docs/updates/README.md b/docs/updates/README.md index d380d896..2f62ff8b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-84 | 2026-09-25 | A run whose window was closed can be stopped from the Run menu | #test #run | [2026-09-c](2026-09-c.md) | | U-20260925-83 | 2026-09-25 | A damaged saved theme stops JEditor's window: recorded | #investigation | [2026-09-c](2026-09-c.md) | | U-20260925-82 | 2026-09-25 | A plugin's About box speaks the IDE language | #fix #i18n #plugins | [2026-09-c](2026-09-c.md) | | U-20260925-81 | 2026-09-25 | architecture_explore.md's line counts re-measured | #docs | [2026-09-c](2026-09-c.md) | @@ -486,4 +487,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 83 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 84 | diff --git a/progress.md b/progress.md index 31e044be..33792bcf 100644 --- a/progress.md +++ b/progress.md @@ -6,7 +6,7 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- ## Open -- **#2** [DECIDE] Closing one run window leaves its child running, with no window and no way to stop it short of a task manager; only closing the whole IDE stops it (`PyBreezeMainWindow.closeEvent`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`). Should closing a run window stop its run (`CodeWindow.stop_runner()` exists; `CodeWindow.closeEvent` today only lets the main window forget a finished one), ask first, or keep going on purpose (for example a long load test that mails its report)? +- **#2** [DECIDE] Closing one run window leaves its child running, with no window; only Run > Stop All Program, which stops every run (`PyBreezeMainWindow.stop_all_runs`), or closing the whole IDE stops it (`PyBreezeMainWindow.closeEvent`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`). Should closing a run window stop its run (`CodeWindow.stop_runner()` exists; `CodeWindow.closeEvent` today only lets the main window forget a finished one), ask first, or keep going on purpose (for example a long load test that mails its report)? - **#27** [DECIDE] paramiko is not pinned (`requirements.txt`, `pyproject.toml`, `dev.toml`), so an install may get 4.x, which still has CVE-2026-44405 (SHA-1 RSA signatures). The code refuses SHA-1 on every connect whatever the version (`SHA1_ALGORITHMS`, U-20260923-40), and the SSH tests pass on 5.0.0. Should the dependency say `paramiko>=5.0.0`, or be pinned exactly like PySide6? - **#49** [DECIDE] jupyterlab is unpinned (`requirements.txt`, `pyproject.toml`, `dev.toml`) and the JupyterLab tab installs it only when it is missing (`jupyter_lab_gui/jupyter_lab_thread.py`, `is_jupyter_installed`), so an existing environment keeps whatever jupyter_server it has; this repo's `.venv` has 2.17.0. Fixed upstream: path traversal (CVE-2026-5422, fixed in 2.18.2), stored XSS through the nbconvert handlers (CVE-2026-44727 / GHSA-fcw5-x6j4-ccmp, 2.20.0; the embedded server has no token, so a script in a rendered notebook can drive it) and tokens logged from the Referer on 5xx (CVE-2026-86049, 2.21.0). jupyterlab itself is 4.5.6 there, below the fixes for a sanitizer-allowed command-linker button that runs commands on one click (CVE-2026-42557, 4.5.7), an SVG opened in the image viewer keeping a same-origin script context (CVE-2026-73415) and an imported `overrides.json` (CVE-2026-73417), both fixed in 4.5.10 / 4.6.2; with no token, a script running in the page can drive the server. Should the dependencies require `jupyter_server>=2.21.0` and `jupyterlab>=4.5.10`, and should the tab check the installed version and offer to upgrade? - **#53** [DECIDE] `requests` and `urllib3` are imported directly (`utils/network/public_http.py`, `http_client.py`, `url_validation.py`, the three AI panels) but declared nowhere (`requirements.txt`, `pyproject.toml`, `dev.toml`); they arrive through the automation packages, so their versions are whatever those allow. This repo's `.venv` has urllib3 2.6.3, below the 2.7.0 that fixes CVE-2026-44431 (Authorization and Cookie forwarded on a cross-origin redirect; these requests do not follow redirects) and CVE-2026-44432 (a Brotli response decompressed whole on a second `read(amt)`; not reproduced here with brotli 1.2.0 installed, `read_capped_text` stopped at its 16 MB cap). `public_http` also relies on urllib3's `_dns_host` and `http.client`'s `_create_connection` (`test_public_http.py` guards both; passes on urllib3 2.6.3 and 2.8.0). `cryptography` is imported directly too (`connect_gui/ssh/ssh_key_loader.py`, reading PKCS#8 and passphrase-protected keys) and arrives only through paramiko; the `.venv` has 46.0.6, below 46.0.7, which fixes CVE-2026-39892 (a non-contiguous buffer passed to an API that takes one, such as `Hash.update(buf[::-1])`; the key loader passes whole `bytes`). So is `qt_material`, which `start_editor` needs to style the window (`editor_main/main_ui.py`) and which arrives only through je-editor; `idna` (`utils/network/url_validation.py`) is imported with a fallback. Should the dependencies declare `requests`, `urllib3>=2.7.0`, `cryptography>=46.0.7` and `qt-material`, and pin them like PySide6? diff --git a/test/test_utils/test_stop_all_runs.py b/test/test_utils/test_stop_all_runs.py index 9d6be2f7..3d244c10 100644 --- a/test/test_utils/test_stop_all_runs.py +++ b/test/test_utils/test_stop_all_runs.py @@ -32,3 +32,38 @@ def stop_runner(self): def test_stop_all_program_stops_every_run_window(tmp_path): # One whose stop fails costs only its own stop; the windows stay, with their output assert run_started_window(tmp_path, _BODY) == {"stopped": 2, "kept": 3} + + +_CLOSED_WINDOW = """ +from pybreeze.extend.process_executor.process_executor_utils import open_run_window + + +class StillRunning: + def poll(self): + return None + + +class Runner: + process = StillRunning() + stopped = 0 + + def stop(self): + Runner.stopped += 1 + + +run_window = open_run_window(window, "a long run") +run_window.runner = Runner() +run_window.show() +run_window.close() +kept = run_window in window.current_run_code_window +window.run_menu.stop_all_program_action.trigger() +result = {"kept after closing": kept, "stopped": Runner.stopped} +run_window.runner = None +window.current_run_code_window.clear() +""" + + +def test_a_run_whose_window_was_closed_can_still_be_stopped(tmp_path): + # Closing a run window lets its run go on (progress #2): Stop All Program + # is how it is stopped without closing the IDE + assert run_started_window(tmp_path, _CLOSED_WINDOW) == {"kept after closing": True, "stopped": 1} From 3579fa52fe1bfdd88b6563587b7462e62cbf7b75 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 13:57:48 +0800 Subject: [PATCH 136/166] Build the guide's PDF with xelatex and CJK fonts --- docs/source/conf.py | 17 +++++++++++++++++ docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8812240e..713d23a9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,3 +33,20 @@ # -- Options for HTML output ------------------------------------------------- html_theme = "sphinx_rtd_theme" + +# -- Options for LaTeX (PDF) output ------------------------------------------- + +# Half of the guide is Traditional Chinese, which pdflatex cannot set: the PDF +# Read the Docs built had every Chinese character missing. xelatex sets it with +# xeCJK and the Noto CJK TC fonts, which Read the Docs' build image has +# (fonts-noto-cjk); ctex's own fonts leave out some Traditional characters. +latex_engine = "xelatex" +latex_use_xindy = False +latex_elements = { + "preamble": "\n".join(( + r"\usepackage{xeCJK}", + r"\setCJKmainfont{Noto Serif CJK TC}", + r"\setCJKsansfont{Noto Sans CJK TC}", + r"\setCJKmonofont{Noto Sans Mono CJK TC}", + )), +} diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 0f026ed9..22ab6314 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -713,3 +713,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_stop_all_runs.py`, `progress.md`, `architecture_explore.md` (test count) - **Checks**: `test_stop_all_runs.py` passes (2); `test/test_utils` collects 2605 tests; `ruff` clean. - **Open items**: `progress.md` #2 (the decision). + +## U-20260925-85 · 2026-09-25 · The PDF of the guide keeps its Chinese half · #fix #docs + +- **What**: Read the Docs builds the guide as HTML, an ePub and a PDF (`.readthedocs.yaml`). The PDF went through Sphinx's default `pdflatex`, which cannot set CJK, and the build still reported success: the downloaded PDF (52 pages) had no Chinese character at all, so the Traditional Chinese half was headings and code with the words missing (the tab example read `EDITOR_EXTEND_TAB.update({"": MyToolWidget})`). The ePub, built from HTML, was whole. Read the Docs adds CJK defaults only to a project whose language is Chinese; this one is English. +- **Change**: `docs/source/conf.py` sets `latex_engine = "xelatex"`, `latex_use_xindy = False`, and a preamble loading `xeCJK` with Noto Serif / Sans / Sans Mono CJK TC, which Read the Docs' ubuntu-22.04 image has (`texlive-full`, `fonts-noto-cjk-extra`, per its Dockerfile). ctex's own default fonts were not used: Read the Docs issue #6319 reports them missing Traditional characters such as 佈 and 換. +- **Files**: `docs/source/conf.py` +- **Checks**: built locally with TinyTeX 2026.09 (xelatex, xeCJK, FreeFont) and the three Noto CJK TC fonts: 64 pages, 10,371 Chinese characters in the text, no missing-glyph warning, 佈 and 換 present. That TeX Live 2026 stops at Sphinx 9.0.4's booktabs table rules (`TeX capacity exceeded` at `\sphinxmidrule`, with or without xeCJK), so the local check used `-D latex_table_style=standard`; Read the Docs' image has TeX Live 2021, where the default table style built before this change. The HTML build gives no warning; the Read the Docs PDF is to be checked after the next build of `latest`. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 2f62ff8b..72d48f26 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-85 | 2026-09-25 | The PDF of the guide keeps its Chinese half | #fix #docs | [2026-09-c](2026-09-c.md) | | U-20260925-84 | 2026-09-25 | A run whose window was closed can be stopped from the Run menu | #test #run | [2026-09-c](2026-09-c.md) | | U-20260925-83 | 2026-09-25 | A damaged saved theme stops JEditor's window: recorded | #investigation | [2026-09-c](2026-09-c.md) | | U-20260925-82 | 2026-09-25 | A plugin's About box speaks the IDE language | #fix #i18n #plugins | [2026-09-c](2026-09-c.md) | @@ -487,4 +488,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 84 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 85 | From 52bee9c2e2ed5d93fd66392c7b2273fdc913200d Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:10:55 +0800 Subject: [PATCH 137/166] Read an arrow's label in linear time --- architecture_explore.md | 6 +-- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- .../diagram_editor/diagram_mermaid_parser.py | 26 ++++++++-- test/test_utils/test_mermaid_parser.py | 49 +++++++++++++++++++ 5 files changed, 83 insertions(+), 9 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 3cc1fc5a..f6bf8f2e 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -243,14 +243,14 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,042 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,058 行,最大子系統) | 檔案 | 職責 | |---|---| | `diagram_editor_widget.py` (695) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | | `diagram_scene.py` (915) | `DiagramScene(QGraphicsScene)`:**State pattern** 的 `ToolMode` 決定滑鼠行為;undo/redo、複製貼上(節點、連線與圖片)、多選對齊與分佈、z-order、序列化 `to_dict()` / `load_from_dict()`。`get_all_nodes/connections/images()` 由下往上列出(`_bottom_first()`),存檔與 undo 還原時同 z 值的重疊項目維持原本的上下;右鍵選單先選取點到的項目;置頂/置底放到所有其他節點與圖片之上/之下;`to_dict()` 給每個節點與圖片記下它在兩者之間由下往上的位置(`stack`),載入後 `_restore_stacking()` 照這個順序重新加回場景(同 z 值時後加的在上面),圖片也存 `z`。`load_from_dict()` 先用 `_check_is_a_diagram()` 確認資料形狀才清空畫布(不合就丟 `ValueError`,畫布原封不動),每一筆節點/連線/圖片再各自容錯;清空前先 `to_dict()` 留一份,載入途中還是出錯就放回原樣再往上丟——載入要嘛成功、要嘛什麼都沒變(編輯器存檔寫回上次開的檔案,半途清空的畫布會蓋掉使用者的檔)。圖片的 `source` 不是字串就丟掉。`undo_scope` 用 `try/finally`,本體丟例外也一定收掉快照;圖片來源先看副檔名、拒絕 UNC(`_is_on_this_machine()`)才碰檔案系統;URL 圖片交給 `ImageDownloadThread(QThread)` 下載並快取在 `_pixmap_cache`,undo/redo 重建項目時直接用快取,不會再連一次網路;編輯器關閉時 `let_image_downloads_run_out()` 把還在跑的下載交給 `let_run_out()`,不在 UI 執行緒等 | | `diagram_items.py` (965) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | -| `diagram_mermaid_parser.py` (613) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | +| `diagram_mermaid_parser.py` (629) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。箭頭的 `|label|` 由 `_arrow_label()` 用兩個相鄰部分不共用字元的樣式讀(引號標籤一個、一般標籤一個,取最左邊的),線性時間:原本合成一個、標籤兩側可有空白的樣式在未閉合的標籤上是立方時間回溯。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | | `diagram_property_panel.py` (453) | 右側屬性側欄,依選取型別切換 node / connection / image 三組表單;每記一步 undo(場景的 `recorded`)就重新整理(在畫布上拖把手改大小時選取沒變);寬、高各自只改自己那一邊;數字欄位不追鍵盤、輸入完才套用 | | `diagram_view.py` (228) | `QGraphicsView`:滾輪與按鈕縮放都經 `_step_zoom()`(有上下界,界外時仍可往界內走;橫向滾輪不縮放),`fit()` 把「符合視窗」夾在上下界內、中鍵或右鍵拖曳平移(右鍵拖過畫布就不開右鍵選單,`contextMenuEvent`;放開沒送到它時下一次移動就停止平移)、`drawBackground` 畫格線 | | `diagram_commands.py` (48) | `DiagramSnapshotCommand(QUndoCommand)` — 快照式 undo,存變更前後完整場景狀態;有 `merge_key` 的連續步驟(同一個項目的同一個屬性:大小、字級、線寬)合併成一步(`id()` / `mergeWith`) | @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2605 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2620 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 22ab6314..4389e5bd 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -721,3 +721,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `docs/source/conf.py` - **Checks**: built locally with TinyTeX 2026.09 (xelatex, xeCJK, FreeFont) and the three Noto CJK TC fonts: 64 pages, 10,371 Chinese characters in the text, no missing-glyph warning, 佈 and 換 present. That TeX Live 2026 stops at Sphinx 9.0.4's booktabs table rules (`TeX capacity exceeded` at `\sphinxmidrule`, with or without xeCJK), so the local check used `-D latex_table_style=standard`; Read the Docs' image has TeX Live 2021, where the default table style built before this change. The HTML build gives no warning; the Read the Docs PDF is to be checked after the next build of `latest`. - **Open items**: none. + +## U-20260925-86 · 2026-09-25 · An arrow's label is read in linear time · #fix #diagram + +- **What**: SonarCloud's open issues on `main` (174: 19 in `pybreeze/`, 155 in tests) include S8786 on the Mermaid importer's arrow-label pattern, `\|\s*("[^"]*"|[^|]*)\s*\|`. Its spaces could be matched by the `\s*` on either side or by the label itself, and on a label with no closing bar that backtracked in cubic time: 800 spaces took 0.5 s, 3,000 about 28 s. The importer itself only hands `_parse_arrow` tokens whose label is closed, which match at once, so a pasted diagram could not reach it; any other caller could. +- **Change**: `_arrow_label()` reads the label with two patterns whose neighbouring parts share no character, one for a quoted label (which may hold a bar) and one for a plain one, and takes the leftmost match, the quoted one when both start at the same bar, as the single pattern's alternation did. 200,000 spaces take 4 ms. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_mermaid_parser.py`, `test/test_utils/test_mermaid_parser.py`, `architecture_explore.md` (the parser's note, its line count and the diagram editor's, the test count) +- **Checks**: `TestArrowLabels`: thirteen tokens with the labels the old pattern gave (recorded before the change), a Hypothesis property with the old pattern as the oracle on tokens of up to 14 characters from bar, quote, space, tab and a letter (200,000 random tokens also compared by hand: no difference), and the time of two 3,000-space unclosed labels (failed before the change: 28 s). The Mermaid parser and diagram editor tests pass (109); `test/test_utils` collects 2620 tests; `ruff` clean. +- **Open items**: the other SonarCloud issues on `main`, next. diff --git a/docs/updates/README.md b/docs/updates/README.md index 72d48f26..45200d25 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-86 | 2026-09-25 | An arrow's label is read in linear time | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-85 | 2026-09-25 | The PDF of the guide keeps its Chinese half | #fix #docs | [2026-09-c](2026-09-c.md) | | U-20260925-84 | 2026-09-25 | A run whose window was closed can be stopped from the Run menu | #test #run | [2026-09-c](2026-09-c.md) | | U-20260925-83 | 2026-09-25 | A damaged saved theme stops JEditor's window: recorded | #investigation | [2026-09-c](2026-09-c.md) | @@ -488,4 +489,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 85 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 86 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_mermaid_parser.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_mermaid_parser.py index 28e9b821..0b9b09b9 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_mermaid_parser.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_mermaid_parser.py @@ -78,6 +78,10 @@ class _EdgeInfo: _LABEL_MAX = 200 # bound non-greedy match to prevent polynomial backtracking on pathological input +# An arrow's |label|: quoted (which may hold a "|"), or anything up to the next "|" +_QUOTED_ARROW_LABEL_RE = re.compile(r'\|\s*("[^"]*")\s*\|') +_PLAIN_ARROW_LABEL_RE = re.compile(r"\|([^|]*)\|") + def _normalize_inline_labels(line: str) -> str: """Convert ``-- label -->`` style to ``-->|label|`` pipe style. @@ -250,13 +254,25 @@ def _parse_node_group(raw: str, nodes: dict[str, _NodeInfo]) -> list[str]: return ids +def _arrow_label(token: str) -> str: + """The ``|label|`` of an arrow token, as written; ``""`` when it has none. + + A quoted label is taken whole: '|"a|b"|' stopped at the "|" inside it. The + two patterns share no characters between neighbouring parts, so each reads + its input once: one pattern for both, spaces allowed on either side of an + unquoted label, backtracked in cubic time. + """ + quoted = _QUOTED_ARROW_LABEL_RE.search(token) + plain = _PLAIN_ARROW_LABEL_RE.search(token) + # The leftmost, and the quoted one where both start at the same "|" + if quoted and (plain is None or quoted.start() <= plain.start()): + return quoted.group(1) + return plain.group(1).strip() if plain else "" + + def _parse_arrow(token: str) -> tuple[str, ConnectionStyle, float]: """Return ``(label, style, line_width)`` from an arrow token.""" - label = "" - # A quoted label is taken whole: '|"a|b"|' stopped at the "|" inside it - lm = re.search(r'\|\s*("[^"]*"|[^|]*)\s*\|', token) - if lm: - label = _unquote(lm.group(1).strip()) + label = _unquote(_arrow_label(token)) if "==" in token: return label, ConnectionStyle.SOLID, 3.5 # thick link diff --git a/test/test_utils/test_mermaid_parser.py b/test/test_utils/test_mermaid_parser.py index 0e68bbdf..db09ada7 100644 --- a/test/test_utils/test_mermaid_parser.py +++ b/test/test_utils/test_mermaid_parser.py @@ -1,6 +1,10 @@ from __future__ import annotations +import re + import pytest +from hypothesis import given, settings +from hypothesis import strategies as st from pybreeze.pybreeze_ui.diagram_editor.diagram_mermaid_parser import ( _order_layers, @@ -381,3 +385,48 @@ def test_percent_signs_in_a_label_are_not_a_comment(self): def test_a_quoted_edge_label_loses_its_quotes(self): assert _edges(parse_mermaid('graph TD\nA -->|"quoted label"| B')) == [("A", "B", "quoted label")] + + +class TestArrowLabels: + @pytest.mark.parametrize("token,label", [ + ("-->", ""), + ("-->|yes|", "yes"), + ("-->| yes |", "yes"), + ('-->|"a|b"|', "a|b"), + ('-->| "a|b" |', "a|b"), + ('-->|"a" b|', '"a" b'), + ('-->|"a|', '"a'), + ("-->||", ""), + ("-->| |", ""), + ("==>|thick|", "thick"), + ('-->|""|', ""), + ("-->|x|y|", "x"), + ("--> |yes|", "yes"), + ]) + def test_the_label_of_an_arrow(self, token, label): + from pybreeze.pybreeze_ui.diagram_editor.diagram_mermaid_parser import _parse_arrow + + assert _parse_arrow(token)[0] == label + + @given(st.text(alphabet='|" a\t', max_size=14)) + @settings(max_examples=400, deadline=None) + def test_the_label_is_the_one_the_old_pattern_found(self, tail): + # The pattern the two replaced, as the oracle: cubic, but not on 14 characters + from pybreeze.pybreeze_ui.diagram_editor.diagram_mermaid_parser import _arrow_label + + old = re.search(r'\|\s*("[^"]*"|[^|]*)\s*\|', "-->" + tail) + + assert _arrow_label("-->" + tail).strip() == (old.group(1).strip() if old else "") + + def test_a_long_unclosed_label_is_read_in_linear_time(self): + # The label pattern backtracked in cubic time: 800 spaces took half a + # second, 3,000 about half a minute, on the UI thread + import time + + from pybreeze.pybreeze_ui.diagram_editor.diagram_mermaid_parser import _parse_arrow + + started = time.perf_counter() + _parse_arrow("-->|" + " " * 3000 + "x") + _parse_arrow('-->| "' + " " * 3000 + "x") + + assert time.perf_counter() - started < 1 From c968345edde04c1b1f3db2c019c54fc7f0efbb87 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:22:55 +0800 Subject: [PATCH 138/166] Deal with SonarCloud's open issues in the package --- architecture_explore.md | 8 ++++---- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- pybreeze/__init__.py | 15 +++++++++------ .../mail_thunder_extend/mail_thunder_setting.py | 2 +- pybreeze/extend_multi_language/extend_english.py | 7 ++++--- .../extend_traditional_chinese.py | 11 ++++++----- .../connect_gui/ssh/ssh_file_viewer_widget.py | 5 +++-- .../pybreeze_ui/diagram_editor/diagram_items.py | 8 ++++++-- .../editor_main/file_tree_context_menu.py | 2 +- .../test_pioneer_menu/build_test_pioneer_menu.py | 2 +- .../menu/plugin_menu/build_run_with_menu.py | 13 +++++++++---- pybreeze/utils/curl_import/request_body.py | 7 +++++-- pybreeze/utils/network/public_http.py | 5 +++-- pybreeze/utils/network/url_validation.py | 6 +++--- pybreeze/utils/terminal_text.py | 11 ++++++----- test/test_utils/test_no_blind_except.py | 12 ++++++++++++ 17 files changed, 83 insertions(+), 42 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index f6bf8f2e..502a7f6d 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -243,13 +243,13 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,058 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,062 行,最大子系統) | 檔案 | 職責 | |---|---| | `diagram_editor_widget.py` (695) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | | `diagram_scene.py` (915) | `DiagramScene(QGraphicsScene)`:**State pattern** 的 `ToolMode` 決定滑鼠行為;undo/redo、複製貼上(節點、連線與圖片)、多選對齊與分佈、z-order、序列化 `to_dict()` / `load_from_dict()`。`get_all_nodes/connections/images()` 由下往上列出(`_bottom_first()`),存檔與 undo 還原時同 z 值的重疊項目維持原本的上下;右鍵選單先選取點到的項目;置頂/置底放到所有其他節點與圖片之上/之下;`to_dict()` 給每個節點與圖片記下它在兩者之間由下往上的位置(`stack`),載入後 `_restore_stacking()` 照這個順序重新加回場景(同 z 值時後加的在上面),圖片也存 `z`。`load_from_dict()` 先用 `_check_is_a_diagram()` 確認資料形狀才清空畫布(不合就丟 `ValueError`,畫布原封不動),每一筆節點/連線/圖片再各自容錯;清空前先 `to_dict()` 留一份,載入途中還是出錯就放回原樣再往上丟——載入要嘛成功、要嘛什麼都沒變(編輯器存檔寫回上次開的檔案,半途清空的畫布會蓋掉使用者的檔)。圖片的 `source` 不是字串就丟掉。`undo_scope` 用 `try/finally`,本體丟例外也一定收掉快照;圖片來源先看副檔名、拒絕 UNC(`_is_on_this_machine()`)才碰檔案系統;URL 圖片交給 `ImageDownloadThread(QThread)` 下載並快取在 `_pixmap_cache`,undo/redo 重建項目時直接用快取,不會再連一次網路;編輯器關閉時 `let_image_downloads_run_out()` 把還在跑的下載交給 `let_run_out()`,不在 UI 執行緒等 | -| `diagram_items.py` (965) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | +| `diagram_items.py` (969) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | | `diagram_mermaid_parser.py` (629) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。箭頭的 `|label|` 由 `_arrow_label()` 用兩個相鄰部分不共用字元的樣式讀(引號標籤一個、一般標籤一個,取最左邊的),線性時間:原本合成一個、標籤兩側可有空白的樣式在未閉合的標籤上是立方時間回溯。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | | `diagram_property_panel.py` (453) | 右側屬性側欄,依選取型別切換 node / connection / image 三組表單;每記一步 undo(場景的 `recorded`)就重新整理(在畫布上拖把手改大小時選取沒變);寬、高各自只改自己那一邊;數字欄位不追鍵盤、輸入完才套用 | | `diagram_view.py` (228) | `QGraphicsView`:滾輪與按鈕縮放都經 `_step_zoom()`(有上下界,界外時仍可往界內走;橫向滾輪不縮放),`fit()` 把「符合視窗」夾在上下界內、中鍵或右鍵拖曳平移(右鍵拖過畫布就不開右鍵選單,`contextMenuEvent`;放開沒送到它時下一次移動就停止平移)、`drawBackground` 畫格線 | @@ -298,7 +298,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 9. `pybreeze_ui/connect_gui/` -### `ssh/`(2,516 行) +### `ssh/`(2,517 行) | 檔案 | 職責 | |---|---| @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2620 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2621 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 4389e5bd..39f4165d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -729,3 +729,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_mermaid_parser.py`, `test/test_utils/test_mermaid_parser.py`, `architecture_explore.md` (the parser's note, its line count and the diagram editor's, the test count) - **Checks**: `TestArrowLabels`: thirteen tokens with the labels the old pattern gave (recorded before the change), a Hypothesis property with the old pattern as the oracle on tokens of up to 14 characters from bar, quote, space, tab and a letter (200,000 random tokens also compared by hand: no difference), and the time of two 3,000-space unclosed labels (failed before the change: 28 s). The Mermaid parser and diagram editor tests pass (109); `test/test_utils` collects 2620 tests; `ruff` clean. - **Open items**: the other SonarCloud issues on `main`, next. + +## U-20260925-87 · 2026-09-25 · SonarCloud's open issues in pybreeze/ dealt with · #refactor #quality + +- **What**: refactor, no change in behaviour. After the arrow-label fix (U-20260925-86), eighteen of SonarCloud's open issues on `main` still applied to `pybreeze/` on `dev` (checked line by line against `origin/main`). +- **Change**: S7632 (3): a comma in a `# noqa: CODE — reason` comment is read as starting another rule code, so the three reasons with commas are reworded, and `test_no_noqa_reason_has_a_comma` keeps new ones out. S5713 (2): `ConnectTimeoutError` alone (urllib3's `NewConnectionError` is one) and `(ImportError, ValueError)` (a `UnicodeError` is a `ValueError`), with comments saying so. S5655 (2): `_number`'s fallback is a type variable, so `None` is a fallback its signature allows. S7504 (2): the SFTP viewer's teardown loops iterate over tuple copies, with the reason. S1192 (2): the facade's module paths and the dictionaries' repeated "Result:" / 「結果:」 are constants. S2589: Run with...'s Save As branch is `_save_as()`, where Sonar no longer assumes the tab still has no file after the dialog. S2583: the float check in `request_body` is two checks with the same message. S5843: the incomplete-escape pattern in `terminal_text` is composed from three named parts (the compiled pattern is byte-for-byte the old one). False positives marked `# NOSONAR — ` as `diagram_scene.py` does: S2068 on two Chinese passphrase messages, S5332 on mounting the checked adapter for `http://`, S1313 on the CGNAT range that is refused. The 155 issues in `test/` (composite asserts, `@pytest.fixture()` parentheses and the like) are not touched here. +- **Files**: `pybreeze/__init__.py`, `pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `pybreeze/pybreeze_ui/diagram_editor/diagram_items.py`, `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py`, `pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py`, `pybreeze/utils/curl_import/request_body.py`, `pybreeze/utils/network/public_http.py`, `pybreeze/utils/network/url_validation.py`, `pybreeze/utils/terminal_text.py`, `test/test_utils/test_no_blind_except.py`, `architecture_explore.md` (line and test counts) +- **Checks**: the tests of every module touched pass (1273, 1 skipped); `test/test_utils` collects 2621 tests; `ruff` clean. Whether SonarCloud agrees shows on its next analysis of `main`. +- **Open items**: the test-code issues, if wanted. diff --git a/docs/updates/README.md b/docs/updates/README.md index 45200d25..51d56845 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-87 | 2026-09-25 | SonarCloud's open issues in pybreeze/ dealt with | #refactor #quality | [2026-09-c](2026-09-c.md) | | U-20260925-86 | 2026-09-25 | An arrow's label is read in linear time | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-85 | 2026-09-25 | The PDF of the guide keeps its Chinese half | #fix #docs | [2026-09-c](2026-09-c.md) | | U-20260925-84 | 2026-09-25 | A run whose window was closed can be stopped from the Run menu | #test #run | [2026-09-c](2026-09-c.md) | @@ -489,4 +490,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 86 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 87 | diff --git a/pybreeze/__init__.py b/pybreeze/__init__.py index 8e682279..d68c7309 100644 --- a/pybreeze/__init__.py +++ b/pybreeze/__init__.py @@ -24,14 +24,17 @@ start_editor, ) +_MAIN_UI = "pybreeze.pybreeze_ui.editor_main.main_ui" +_JEDITOR = "je_editor" + # Where each public name lives; JEditor's plugin API is re-exported for convenience _HOMES = { - "EDITOR_EXTEND_TAB": "pybreeze.pybreeze_ui.editor_main.main_ui", - "PyBreezeMainWindow": "pybreeze.pybreeze_ui.editor_main.main_ui", - "start_editor": "pybreeze.pybreeze_ui.editor_main.main_ui", - "load_external_plugins": "je_editor", - "register_natural_language": "je_editor", - "register_programming_language": "je_editor", + "EDITOR_EXTEND_TAB": _MAIN_UI, + "PyBreezeMainWindow": _MAIN_UI, + "start_editor": _MAIN_UI, + "load_external_plugins": _JEDITOR, + "register_natural_language": _JEDITOR, + "register_programming_language": _JEDITOR, } __all__ = [ diff --git a/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py b/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py index f814b37b..be2cca7b 100644 --- a/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py +++ b/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py @@ -47,7 +47,7 @@ def send() -> None: outcome = send_report(html_report_path, not_before=not_before) # The last stop on this thread: anything je_mail_thunder raises past # send_report must still reach on_done, or the run window never says - except Exception as error: # noqa: BLE001 — logged, and reported to the run window + except Exception as error: # noqa: BLE001 — logged and reported to the run window pybreeze_logger.error("Sending the report failed: %r", error) outcome = f"sending failed ({type(error).__name__})" if on_done is not None: diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 68e98c22..891edaf2 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -9,6 +9,7 @@ _COT_PROMPT_EDITOR = "CoT Prompt Editor" _SKILL_PROMPT_EDITOR = "Skill Prompt Editor" +_RESULT_LABEL = "Result:" # PyBreeze-specific English translations pybreeze_english_word_dict = { @@ -533,7 +534,7 @@ "ctrl_enter_when_not_json": "Ctrl+Enter, when the input is not a JSON object", "query_json_to_json_button": "Query → JSON", "query_json_to_query_button": "JSON → Query", - "query_json_output_label": "Result:", + "query_json_output_label": _RESULT_LABEL, "query_json_error": "Could not convert: {error}", "query_json_empty_hint": "Enter a query string or a JSON object above.", # URL Parser / Builder — Menu @@ -547,7 +548,7 @@ "https://user@host:8080/path?a=1#frag or {\"scheme\": \"https\", \"host\": \"host\"}", "url_builder_to_json_button": "URL → JSON", "url_builder_to_url_button": "JSON → URL", - "url_builder_output_label": "Result:", + "url_builder_output_label": _RESULT_LABEL, "url_builder_parse_error": "Could not read the URL: {error}", "url_builder_error": "Could not build URL: {error}", "url_builder_empty_hint": "Enter a URL or a JSON object of URL parts above.", @@ -607,7 +608,7 @@ "json_format_input_placeholder": "{\"a\": 1, \"b\": [2, 3]}", "json_format_format_button": "Format", "json_format_minify_button": "Minify", - "json_format_output_label": "Result:", + "json_format_output_label": _RESULT_LABEL, "json_format_error": "Invalid JSON: {error}", "json_format_empty_hint": "Paste JSON above, then format or minify.", # Response Inspector — Menu diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index eda5baab..dc586d5f 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -4,6 +4,7 @@ _COT_PROMPT_EDITOR = "CoT 提示詞編輯器" _SKILL_PROMPT_EDITOR = "Skill 提示詞編輯器" +_RESULT_LABEL = "結果:" # PyBreeze-specific Traditional Chinese translations pybreeze_traditional_chinese_word_dict = { @@ -169,8 +170,8 @@ "ssh_command_widget_dialog_title_key_error": "金鑰錯誤", "ssh_command_widget_dialog_message_key_file_not_exist": "金鑰檔案不存在。", "ssh_command_widget_error_message_unsupported_private_key": "不支援或無效的私鑰。", - "ssh_key_error_passphrase_needed": "這把私鑰有密語保護:請在密語欄輸入。", - "ssh_key_error_passphrase_wrong": "私鑰的密語不對。", + "ssh_key_error_passphrase_needed": "這把私鑰有密語保護:請在密語欄輸入。", # NOSONAR S2068 — a message, not a credential + "ssh_key_error_passphrase_wrong": "私鑰的密語不對。", # NOSONAR S2068 — a message, not a credential "ssh_key_error_putty_key": "這是 PuTTY 金鑰(.ppk),這裡無法使用:請在 PuTTYgen 載入它," "選 Conversions > Export OpenSSH key 匯出,再選擇匯出的檔案。", "ssh_command_widget_error_message_key_auth_failed": "金鑰驗證失敗", @@ -495,7 +496,7 @@ "ctrl_enter_when_not_json": "Ctrl+Enter(輸入不是 JSON 物件時)", "query_json_to_json_button": "Query → JSON", "query_json_to_query_button": "JSON → Query", - "query_json_output_label": "結果:", + "query_json_output_label": _RESULT_LABEL, "query_json_error": "無法轉換:{error}", "query_json_empty_hint": "請在上方輸入查詢字串或 JSON 物件。", # URL 解析/組建器 — 選單 @@ -509,7 +510,7 @@ "https://user@host:8080/path?a=1#frag 或 {\"scheme\": \"https\", \"host\": \"host\"}", "url_builder_to_json_button": "URL → JSON", "url_builder_to_url_button": "JSON → URL", - "url_builder_output_label": "結果:", + "url_builder_output_label": _RESULT_LABEL, "url_builder_parse_error": "無法解析這個 URL:{error}", "url_builder_error": "無法組建 URL:{error}", "url_builder_empty_hint": "請在上方輸入 URL 或描述 URL 各部分的 JSON 物件。", @@ -569,7 +570,7 @@ "json_format_input_placeholder": "{\"a\": 1, \"b\": [2, 3]}", "json_format_format_button": "格式化", "json_format_minify_button": "壓縮", - "json_format_output_label": "結果:", + "json_format_output_label": _RESULT_LABEL, "json_format_error": "無效的 JSON:{error}", "json_format_empty_hint": "請在上方貼上 JSON,再按格式化或壓縮。", # 回應檢視器 — 選單 diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index f081a5fe..c6753fdf 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -238,11 +238,12 @@ def closeEvent(self, event) -> None: if transfer_running: let_run_out(transfer, transfer.done, transfer.failed, transfer.cancelled) transfer.finished.connect(self.client.close) - for listing in list(self._listings): + # Over copies: a thread's slot may drop it from its set + for listing in tuple(self._listings): if listing.isRunning(): let_run_out(listing, listing.listed, listing.failed) self._listings.clear() - for call in list(self._calls): + for call in tuple(self._calls): if call.isRunning(): let_run_out(call, call.done, call.failed) self._calls.clear() diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py index 2f6dce3b..7e05039a 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py @@ -3,6 +3,7 @@ import math from dataclasses import dataclass from enum import Enum, auto +from typing import TypeVar from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import ( @@ -155,8 +156,11 @@ def _safe_color(value: str | None, fallback: str) -> QColor: _MAX_LINE_WIDTH = 10.0 -def _number(value: object, fallback: float) -> float: - """Return *value* as a float, or *fallback* when it is not a finite number.""" +_Fallback = TypeVar("_Fallback") + + +def _number(value: object, fallback: _Fallback) -> float | _Fallback: + """Return *value* as a float, or *fallback* (a number, or ``None``) when it is not a finite number.""" try: number = float(value) except (TypeError, ValueError): diff --git a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py index 1e3e5f0a..502d9ed1 100644 --- a/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py +++ b/pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py @@ -271,7 +271,7 @@ def _unwatch(editor: EditorWidget) -> None: Done before the file moves: once it is gone, Windows does not let go of the old name. """ - watcher = editor._file_watcher # noqa: SLF001 — JEditor's own watcher, handled as open_an_file handles it (test_jeditor_contract.py) + watcher = editor._file_watcher # noqa: SLF001 — JEditor's own watcher; handled as open_an_file handles it (test_jeditor_contract.py) watched = watcher.files() if watched: watcher.removePaths(watched) diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py index eb458970..109b2a43 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py @@ -82,7 +82,7 @@ def create_template(ui_we_want_to_set: PyBreezeMainWindow) -> None: return try: create_template_dir(project_path=str(project), parent_name=_TEMPLATE_DIR) - except Exception as error: # noqa: BLE001 — TestPioneer raises its unexported ProjectException, a bare Exception subclass, for a write it could not make; it is logged and reported + except Exception as error: # noqa: BLE001 — TestPioneer raises its unexported ProjectException (a bare Exception subclass) for a write it could not make; it is logged and reported pybreeze_logger.error("TestPioneer template not created in %s: %r", project, error) QMessageBox.warning( ui_we_want_to_set, title, diff --git a/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py b/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py index b65d347e..deea9d7a 100644 --- a/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py +++ b/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py @@ -103,10 +103,7 @@ def save_current_file_for_run(main_window: PyBreezeMainWindow) -> str | None: def _save(main_window: PyBreezeMainWindow, widget: EditorWidget) -> str | None: """Write *widget* to its file, or through Save As; the path, or None if cancelled.""" if not widget.current_file: - if not choose_file_get_save_file_path(main_window): - return None - # The save dialog can be accepted without a path being set. - return widget.current_file or None + return _save_as(main_window, widget) write_file_with_encoding( str(widget.current_file), widget.code_edit.toPlainText(), getattr(widget, "file_encoding", DEFAULT_ENCODING), @@ -119,6 +116,14 @@ def _save(main_window: PyBreezeMainWindow, widget: EditorWidget) -> str | None: return widget.current_file +def _save_as(main_window: PyBreezeMainWindow, widget: EditorWidget) -> str | None: + """Save a tab that has no file through JEditor's Save As; its new path, or None if cancelled.""" + if not choose_file_get_save_file_path(main_window): + return None + # The dialog sets the tab's file; it can be accepted without a path being set. + return widget.current_file or None + + def run_current_file_with(main_window: PyBreezeMainWindow, run_config: dict) -> None: """Save the current file, then run it with *run_config* in a new run window.""" file_path = save_current_file_for_run(main_window) diff --git a/pybreeze/utils/curl_import/request_body.py b/pybreeze/utils/curl_import/request_body.py index 0402b6da..ce52e8a0 100644 --- a/pybreeze/utils/curl_import/request_body.py +++ b/pybreeze/utils/curl_import/request_body.py @@ -75,8 +75,11 @@ def _exact_float(text: str) -> float: :raises ValueError: when it is not (too many digits, or out of range) """ value = float(text) - if not math.isfinite(value) or Decimal(repr(value)) != Decimal(text): - raise ValueError(f"{text} does not survive as a float") + survives = f"{text} does not survive as a float" + if not math.isfinite(value): + raise ValueError(survives) + if Decimal(repr(value)) != Decimal(text): + raise ValueError(survives) return value diff --git a/pybreeze/utils/network/public_http.py b/pybreeze/utils/network/public_http.py index 44c7451c..ad682cd5 100644 --- a/pybreeze/utils/network/public_http.py +++ b/pybreeze/utils/network/public_http.py @@ -150,7 +150,8 @@ def _new_conn(self) -> socket.socket: self._dns_host = address try: return super()._new_conn() # type: ignore[misc] - except (NewConnectionError, ConnectTimeoutError) as error: + # A refused connection too: urllib3's NewConnectionError is a ConnectTimeoutError + except ConnectTimeoutError as error: failure = error finally: self._dns_host = name @@ -208,7 +209,7 @@ def public_session() -> requests.Session: """ session = _NoRedirectSession() adapter = PublicAddressAdapter() - session.mount("http://", adapter) + session.mount("http://", adapter) # NOSONAR S5332 — the checked adapter for http URLs; which schemes are allowed is validate_url's call session.mount("https://", adapter) return session diff --git a/pybreeze/utils/network/url_validation.py b/pybreeze/utils/network/url_validation.py index f5074418..048c8674 100644 --- a/pybreeze/utils/network/url_validation.py +++ b/pybreeze/utils/network/url_validation.py @@ -23,7 +23,7 @@ # RFC 6598 shared address space (Carrier-Grade NAT). Not covered by # ``is_private`` / ``is_reserved`` yet routinely abused for SSRF in cloud # environments, so it is blocked explicitly. -_CGNAT_NETWORK = ipaddress.ip_network("100.64.0.0/10") +_CGNAT_NETWORK = ipaddress.ip_network("100.64.0.0/10") # NOSONAR S1313 — a range refused, not an address connected to # RFC 6052 NAT64 well-known prefix. The trailing 32 bits embed an IPv4 target # that a NAT64 gateway routes to, so it must be decoded and re-checked. @@ -115,8 +115,8 @@ def _as_ascii(host: str) -> str: try: import idna return idna.encode(host.lower(), strict=True, std3_rules=True).decode("ascii") - except (ImportError, UnicodeError, ValueError): - # idna.IDNAError is a UnicodeError + except (ImportError, ValueError): + # idna.IDNAError is a UnicodeError, which is a ValueError return host diff --git a/pybreeze/utils/terminal_text.py b/pybreeze/utils/terminal_text.py index 911735d1..69b8c098 100644 --- a/pybreeze/utils/terminal_text.py +++ b/pybreeze/utils/terminal_text.py @@ -25,11 +25,12 @@ # Backspace is applied first (_apply_backspaces) _CONTROL_CHARACTER = re.compile('[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]') -# The end of a read that stops inside an escape sequence: a lone ESC, a CSI -# still waiting for its final byte, a control string still waiting for its -# terminator (or the second byte of ST), or a character-set escape still -# waiting for its final byte -_INCOMPLETE_ESCAPE = re.compile(r'\x1B(?:\[[0-?]*[ -/]*|[\]PX^_][^\x07\x1B]*\x1B?|[ -/]+)?\Z') +# The end of a read that stops inside an escape sequence: a lone ESC, or ESC and +# one of these, still waiting for the rest +_UNFINISHED_CSI = r'\[[0-?]*[ -/]*' # a CSI without its final byte +_UNFINISHED_STRING = r'[\]PX^_][^\x07\x1B]*\x1B?' # a control string without its terminator (or ST's second byte) +_UNFINISHED_CHARSET = r'[ -/]+' # a character-set escape without its final byte +_INCOMPLETE_ESCAPE = re.compile(rf'\x1B(?:{_UNFINISHED_CSI}|{_UNFINISHED_STRING}|{_UNFINISHED_CHARSET})?\Z') # Longest such tail held back for the next read; anything longer is shown as is _MAX_PENDING_ESCAPE = 256 diff --git a/test/test_utils/test_no_blind_except.py b/test/test_utils/test_no_blind_except.py index 231dadb6..a8216a24 100644 --- a/test/test_utils/test_no_blind_except.py +++ b/test/test_utils/test_no_blind_except.py @@ -41,3 +41,15 @@ def test_every_blind_catch_re_raises_or_says_why(): continue offenders.append(f"{path.relative_to(PACKAGE.parent)}:{node.lineno}") assert offenders == [] + + +def test_no_noqa_reason_has_a_comma(): + # SonarCloud reads what follows a comma in "# noqa: CODE — reason" as more + # rule codes, and reports the comment as a malformed suppression (S7632) + offenders = [] + for path in sorted(PACKAGE.rglob("*.py")): + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + _, marker, rest = line.partition("# noqa:") + if marker and "," in rest.partition("—")[2]: + offenders.append(f"{path.relative_to(PACKAGE)}:{number}") + assert not offenders, offenders From 0b21ceeca923095e25695cf5b11b7471b4ae7c66 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:24:28 +0800 Subject: [PATCH 139/166] Write fixture decorators without empty parentheses --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- test/test_utils/test_ai_code_review_client.py | 2 +- test/test_utils/test_ai_request_formats.py | 2 +- test/test_utils/test_autocontrol_record.py | 2 +- test/test_utils/test_context_menus_are_freed.py | 2 +- test/test_utils/test_create_project.py | 4 ++-- test/test_utils/test_curl_import_gui.py | 4 ++-- test/test_utils/test_diagram_align.py | 2 +- test/test_utils/test_diagram_editor_widget.py | 2 +- test/test_utils/test_diagram_images.py | 4 ++-- test/test_utils/test_diagram_property_panel.py | 4 ++-- test/test_utils/test_diff_gui.py | 2 +- test/test_utils/test_error_text.py | 2 +- test/test_utils/test_file_tree_context_menu.py | 4 ++-- test/test_utils/test_gui_thread_gc.py | 2 +- test/test_utils/test_har_import_gui.py | 4 ++-- test/test_utils/test_hash_gui.py | 2 +- test/test_utils/test_header_analyzer_gui.py | 2 +- test/test_utils/test_http_status_gui.py | 4 ++-- test/test_utils/test_image_download.py | 4 ++-- test/test_utils/test_install_menu.py | 2 +- test/test_utils/test_json_format_gui.py | 2 +- test/test_utils/test_jupyter_lifecycle.py | 2 +- test/test_utils/test_jupyter_ready.py | 2 +- test/test_utils/test_jwt_decoder_gui.py | 2 +- test/test_utils/test_long_scripts.py | 2 +- test/test_utils/test_mail_report.py | 6 +++--- test/test_utils/test_plugin_menu.py | 4 ++-- test/test_utils/test_prompt_store.py | 2 +- test/test_utils/test_prthinker_contract.py | 2 +- test/test_utils/test_prthinker_menu_review.py | 2 +- test/test_utils/test_prthinker_process.py | 4 ++-- test/test_utils/test_prthinker_setting.py | 2 +- test/test_utils/test_prthinker_setting_dialog.py | 4 ++-- test/test_utils/test_public_http.py | 8 ++++---- test/test_utils/test_query_json_gui.py | 2 +- test/test_utils/test_regex_gui.py | 2 +- test/test_utils/test_response_inspector_gui.py | 4 ++-- test/test_utils/test_run_folder.py | 2 +- test/test_utils/test_run_window_title.py | 2 +- test/test_utils/test_sftp_tree_actions.py | 2 +- test/test_utils/test_sftp_upload.py | 2 +- test/test_utils/test_skills_panel.py | 4 ++-- test/test_utils/test_ssh_host_key_policy.py | 2 +- test/test_utils/test_ssh_login_form.py | 2 +- test/test_utils/test_test_pioneer_template.py | 2 +- test/test_utils/test_testpioneer_menu.py | 2 +- test/test_utils/test_timestamp_gui.py | 2 +- test/test_utils/test_tool_output_wording.py | 4 ++-- test/test_utils/test_unsaved_on_close.py | 2 +- test/test_utils/test_url_builder_gui.py | 2 +- test/test_utils/test_url_parser_differential.py | 2 +- 53 files changed, 80 insertions(+), 71 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 39f4165d..76f08c32 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -737,3 +737,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/__init__.py`, `pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py`, `pybreeze/extend_multi_language/extend_english.py`, `pybreeze/extend_multi_language/extend_traditional_chinese.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py`, `pybreeze/pybreeze_ui/diagram_editor/diagram_items.py`, `pybreeze/pybreeze_ui/editor_main/file_tree_context_menu.py`, `pybreeze/pybreeze_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py`, `pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py`, `pybreeze/utils/curl_import/request_body.py`, `pybreeze/utils/network/public_http.py`, `pybreeze/utils/network/url_validation.py`, `pybreeze/utils/terminal_text.py`, `test/test_utils/test_no_blind_except.py`, `architecture_explore.md` (line and test counts) - **Checks**: the tests of every module touched pass (1273, 1 skipped); `test/test_utils` collects 2621 tests; `ruff` clean. Whether SonarCloud agrees shows on its next analysis of `main`. - **Open items**: the test-code issues, if wanted. + +## U-20260925-88 · 2026-09-25 · Fixture decorators without empty parentheses · #refactor #test + +- **What**: refactor of test code, no change in behaviour. SonarCloud reported 59 decorators written with empty parentheses (S9083) on `main`; `dev` has 70, all `@pytest.fixture()`, which pytest treats exactly as `@pytest.fixture`. +- **Change**: the 70 are `@pytest.fixture` (a sed over the 51 files that had one; fixtures with arguments are untouched). +- **Files**: 51 files in `test/test_utils/` +- **Checks**: `test/test_utils` still collects 2621 tests; a sample of files using these fixtures passes (173). The other test-code issues (composite asserts, S9073) are left: splitting 78 asserts by hand is churn for a clearer failure message only. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 51d56845..b5004d43 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-88 | 2026-09-25 | Fixture decorators without empty parentheses | #refactor #test | [2026-09-c](2026-09-c.md) | | U-20260925-87 | 2026-09-25 | SonarCloud's open issues in pybreeze/ dealt with | #refactor #quality | [2026-09-c](2026-09-c.md) | | U-20260925-86 | 2026-09-25 | An arrow's label is read in linear time | #fix #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-85 | 2026-09-25 | The PDF of the guide keeps its Chinese half | #fix #docs | [2026-09-c](2026-09-c.md) | @@ -490,4 +491,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 87 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 88 | diff --git a/test/test_utils/test_ai_code_review_client.py b/test/test_utils/test_ai_code_review_client.py index 7436e3e6..c92bc257 100644 --- a/test/test_utils/test_ai_code_review_client.py +++ b/test/test_utils/test_ai_code_review_client.py @@ -29,7 +29,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def client(app, tmp_path, monkeypatch): monkeypatch.setattr(ai_code_review_gui, "pybreeze_data_dir", lambda: tmp_path) made = AICodeReviewClient() diff --git a/test/test_utils/test_ai_request_formats.py b/test/test_utils/test_ai_request_formats.py index a04bf6b3..8ea5792d 100644 --- a/test/test_utils/test_ai_request_formats.py +++ b/test/test_utils/test_ai_request_formats.py @@ -45,7 +45,7 @@ def post(self, url, **kwargs): return self.request("POST", url, **kwargs) -@pytest.fixture() +@pytest.fixture def session(monkeypatch): recorder = _Session() for module in (ai_code_review_gui, skills_send_gui): diff --git a/test/test_utils/test_autocontrol_record.py b/test/test_utils/test_autocontrol_record.py index 7b4e5c67..dd8ff832 100644 --- a/test/test_utils/test_autocontrol_record.py +++ b/test/test_utils/test_autocontrol_record.py @@ -31,7 +31,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def window(app, monkeypatch): monkeypatch.setattr(record_menu, "EditorWidget", EditorTab) made = QMainWindow() diff --git a/test/test_utils/test_context_menus_are_freed.py b/test/test_utils/test_context_menus_are_freed.py index 62535b3a..2216ace2 100644 --- a/test/test_utils/test_context_menus_are_freed.py +++ b/test/test_utils/test_context_menus_are_freed.py @@ -29,7 +29,7 @@ def exec(self, *args): return None -@pytest.fixture() +@pytest.fixture def menus_dismissed(monkeypatch): # Setting exec on PySide's QMenu itself does not reach the call for module in (file_tree_context_menu, ssh_file_viewer_widget): diff --git a/test/test_utils/test_create_project.py b/test/test_utils/test_create_project.py index d2b88b55..91e0481a 100644 --- a/test/test_utils/test_create_project.py +++ b/test/test_utils/test_create_project.py @@ -29,7 +29,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def package(monkeypatch): """A package whose create_project_dir writes one template file, as the real ones do.""" fake = types.ModuleType(_PACKAGE) @@ -48,7 +48,7 @@ def create_project_dir(project_path: str | None = None, parent_name: str = "Fake return fake -@pytest.fixture() +@pytest.fixture def dialogs(monkeypatch): """Record every message box; ``answer`` is what a question gets.""" shown: dict = {"question": [], "warning": [], "information": [], "answer": QMessageBox.StandardButton.No} diff --git a/test/test_utils/test_curl_import_gui.py b/test/test_utils/test_curl_import_gui.py index 8c673765..fa23e110 100644 --- a/test/test_utils/test_curl_import_gui.py +++ b/test/test_utils/test_curl_import_gui.py @@ -22,7 +22,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.curl_import_gui import CurlImportGUI gui = CurlImportGUI() @@ -128,7 +128,7 @@ def __init__(self, _main_window): self.code_edit = type("CodeEdit", (), {"setPlainText": lambda self, t: setattr(self, "text", t)})() -@pytest.fixture() +@pytest.fixture def widget_with_window(app): from pybreeze.pybreeze_ui.tools_gui.curl_import_gui import CurlImportGUI window = _FakeMainWindow() diff --git a/test/test_utils/test_diagram_align.py b/test/test_utils/test_diagram_align.py index b36a32a0..56869358 100644 --- a/test/test_utils/test_diagram_align.py +++ b/test/test_utils/test_diagram_align.py @@ -20,7 +20,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def scene(app): diagram = DiagramScene() yield diagram diff --git a/test/test_utils/test_diagram_editor_widget.py b/test/test_utils/test_diagram_editor_widget.py index 45c241b0..91ecfb6f 100644 --- a/test/test_utils/test_diagram_editor_widget.py +++ b/test/test_utils/test_diagram_editor_widget.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def editor(app): from pybreeze.pybreeze_ui.diagram_editor.diagram_editor_widget import DiagramEditorWidget diff --git a/test/test_utils/test_diagram_images.py b/test/test_utils/test_diagram_images.py index dc716f78..e134f7d9 100644 --- a/test/test_utils/test_diagram_images.py +++ b/test/test_utils/test_diagram_images.py @@ -50,7 +50,7 @@ def an_image() -> bytes: return bytes(data.data()) -@pytest.fixture() +@pytest.fixture def downloads(monkeypatch) -> list: """Record every fetch, and answer each with a real image.""" asked: list = [] @@ -161,7 +161,7 @@ def _wait_until(app, condition) -> None: time.sleep(0.01) -@pytest.fixture() +@pytest.fixture def slow_host(monkeypatch) -> threading.Event: """A host that answers with an image once the returned event is set.""" answering = threading.Event() diff --git a/test/test_utils/test_diagram_property_panel.py b/test/test_utils/test_diagram_property_panel.py index 8cfe00cd..16e885b8 100644 --- a/test/test_utils/test_diagram_property_panel.py +++ b/test/test_utils/test_diagram_property_panel.py @@ -24,7 +24,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def connection(app): scene = DiagramScene() source, target = DiagramNode(x=0, y=0, text="A"), DiagramNode(x=300, y=0, text="B") @@ -39,7 +39,7 @@ def connection(app): scene.deleteLater() -@pytest.fixture() +@pytest.fixture def image(app): scene = DiagramScene() picture = DiagramImage(x=0, y=0, w=120, h=80) diff --git a/test/test_utils/test_diff_gui.py b/test/test_utils/test_diff_gui.py index ba4a83ba..4b4c3748 100644 --- a/test/test_utils/test_diff_gui.py +++ b/test/test_utils/test_diff_gui.py @@ -18,7 +18,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.diff_gui import DiffGUI gui = DiffGUI() diff --git a/test/test_utils/test_error_text.py b/test/test_utils/test_error_text.py index bec3a280..a2d11802 100644 --- a/test/test_utils/test_error_text.py +++ b/test/test_utils/test_error_text.py @@ -25,7 +25,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def chinese(monkeypatch): monkeypatch.setattr(error_text_mod.language_wrapper, "language_word_dict", CHINESE) diff --git a/test/test_utils/test_file_tree_context_menu.py b/test/test_utils/test_file_tree_context_menu.py index 67f36295..def5da2d 100644 --- a/test/test_utils/test_file_tree_context_menu.py +++ b/test/test_utils/test_file_tree_context_menu.py @@ -34,7 +34,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def tree(app, tmp_path): """A tree view rooted at *tmp_path*, as the project tree would be.""" view = QTreeView() @@ -82,7 +82,7 @@ def move_to_trash(path: Path) -> bool: return trashed -@pytest.fixture() +@pytest.fixture def warnings(monkeypatch): """Collect the warning dialogs an action raises instead of showing them.""" shown: list[str] = [] diff --git a/test/test_utils/test_gui_thread_gc.py b/test/test_utils/test_gui_thread_gc.py index bef3874e..6b64edcc 100644 --- a/test/test_utils/test_gui_thread_gc.py +++ b/test/test_utils/test_gui_thread_gc.py @@ -14,7 +14,7 @@ from pybreeze.pybreeze_ui.gui_thread_gc import GuiThreadGarbageCollector -@pytest.fixture() +@pytest.fixture def collector(): was_enabled = gc.isenabled() made = GuiThreadGarbageCollector() diff --git a/test/test_utils/test_har_import_gui.py b/test/test_utils/test_har_import_gui.py index 2ad9d2c2..72c3b39f 100644 --- a/test/test_utils/test_har_import_gui.py +++ b/test/test_utils/test_har_import_gui.py @@ -46,7 +46,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.har_import_gui import HarImportGUI gui = HarImportGUI() @@ -55,7 +55,7 @@ def widget(app): gui.deleteLater() -@pytest.fixture() +@pytest.fixture def loaded(widget): widget.load_text(_HAR) return widget diff --git a/test/test_utils/test_hash_gui.py b/test/test_utils/test_hash_gui.py index a1c09348..b596bade 100644 --- a/test/test_utils/test_hash_gui.py +++ b/test/test_utils/test_hash_gui.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.hash_gui import HashGUI gui = HashGUI() diff --git a/test/test_utils/test_header_analyzer_gui.py b/test/test_utils/test_header_analyzer_gui.py index 6b4b9451..ee335608 100644 --- a/test/test_utils/test_header_analyzer_gui.py +++ b/test/test_utils/test_header_analyzer_gui.py @@ -27,7 +27,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.header_analyzer_gui import HeaderAnalyzerGUI gui = HeaderAnalyzerGUI() diff --git a/test/test_utils/test_http_status_gui.py b/test/test_utils/test_http_status_gui.py index 60057bf4..41b6db84 100644 --- a/test/test_utils/test_http_status_gui.py +++ b/test/test_utils/test_http_status_gui.py @@ -18,7 +18,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.http_status_gui import HttpStatusGUI gui = HttpStatusGUI() @@ -75,7 +75,7 @@ def test_has_output_actions(self, widget): class TestTheCategoryInTheIdeLanguage: """The class of a status ([Client Error]) was English in the Traditional Chinese IDE.""" - @pytest.fixture() + @pytest.fixture def chinese(self, app, monkeypatch): from je_editor import language_wrapper diff --git a/test/test_utils/test_image_download.py b/test/test_utils/test_image_download.py index 39b9f858..f17b380a 100644 --- a/test/test_utils/test_image_download.py +++ b/test/test_utils/test_image_download.py @@ -55,14 +55,14 @@ def close(self) -> None: self._httpd.server_close() -@pytest.fixture() +@pytest.fixture def loopback_only(monkeypatch): """127.0.0.1 may stand in for a public server; every other private address stays refused.""" blocked = url_validation._is_blocked_ip monkeypatch.setattr(url_validation, "_is_blocked_ip", lambda ip: ip != _LOOPBACK and blocked(ip)) -@pytest.fixture() +@pytest.fixture def serve(): servers: list[_Server] = [] diff --git a/test/test_utils/test_install_menu.py b/test/test_utils/test_install_menu.py index b9556e25..46362b0b 100644 --- a/test/test_utils/test_install_menu.py +++ b/test/test_utils/test_install_menu.py @@ -31,7 +31,7 @@ def start_module_process(self, package, arguments, environment=None) -> None: self.runs.append((package, list(arguments))) -@pytest.fixture() +@pytest.fixture def processes(monkeypatch) -> list: started: list = [] diff --git a/test/test_utils/test_json_format_gui.py b/test/test_utils/test_json_format_gui.py index 93fc48d7..fbcf6655 100644 --- a/test/test_utils/test_json_format_gui.py +++ b/test/test_utils/test_json_format_gui.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.json_format_gui import JsonFormatGUI gui = JsonFormatGUI() diff --git a/test/test_utils/test_jupyter_lifecycle.py b/test/test_utils/test_jupyter_lifecycle.py index b4ddb465..2234720e 100644 --- a/test/test_utils/test_jupyter_lifecycle.py +++ b/test/test_utils/test_jupyter_lifecycle.py @@ -44,7 +44,7 @@ def poll(self): return self.returncode -@pytest.fixture() +@pytest.fixture def launched(monkeypatch) -> list: """Start the launcher's run() against a fake server and collect what it started.""" started: list = [] diff --git a/test/test_utils/test_jupyter_ready.py b/test/test_utils/test_jupyter_ready.py index eea7e3ad..6ad9ccc7 100644 --- a/test/test_utils/test_jupyter_ready.py +++ b/test/test_utils/test_jupyter_ready.py @@ -77,7 +77,7 @@ def test_returns_when_port_open(self, qt_app, monkeypatch): class TestTheReasonIsInTheIdeLanguage: """A start that timed out or a server that exited read in English whatever the IDE spoke.""" - @pytest.fixture() + @pytest.fixture def chinese(self, monkeypatch): from pybreeze.extend_multi_language.extend_traditional_chinese import ( pybreeze_traditional_chinese_word_dict as word, diff --git a/test/test_utils/test_jwt_decoder_gui.py b/test/test_utils/test_jwt_decoder_gui.py index 610eea1c..e1e4d23b 100644 --- a/test/test_utils/test_jwt_decoder_gui.py +++ b/test/test_utils/test_jwt_decoder_gui.py @@ -27,7 +27,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.jwt_decoder_gui import JwtDecoderGUI gui = JwtDecoderGUI() diff --git a/test/test_utils/test_long_scripts.py b/test/test_utils/test_long_scripts.py index 91373cc1..01ae195f 100644 --- a/test/test_utils/test_long_scripts.py +++ b/test/test_utils/test_long_scripts.py @@ -34,7 +34,7 @@ def app(): return QApplication.instance() or QApplication([]) -@pytest.fixture() +@pytest.fixture def reporter(tmp_path, monkeypatch): """A package that says which flag it was given and what it read, in ASCII.""" (tmp_path / "report_args.py").write_text(_REPORTER, encoding="utf-8") diff --git a/test/test_utils/test_mail_report.py b/test/test_utils/test_mail_report.py index 2cbc4e32..5e79ab56 100644 --- a/test/test_utils/test_mail_report.py +++ b/test/test_utils/test_mail_report.py @@ -51,7 +51,7 @@ class MailThunderException(Exception): pass -@pytest.fixture() +@pytest.fixture def mail_thunder(monkeypatch): """A stand-in je_mail_thunder with a user in its content file.""" FakeSmtp.instances = [] @@ -69,14 +69,14 @@ def mail_thunder(monkeypatch): return package -@pytest.fixture() +@pytest.fixture def logger(monkeypatch) -> MagicMock: fake = MagicMock() monkeypatch.setattr(mail, "pybreeze_logger", fake) return fake -@pytest.fixture() +@pytest.fixture def report(tmp_path) -> str: path = tmp_path / "report.html" path.write_text("ok", encoding="utf-8") diff --git a/test/test_utils/test_plugin_menu.py b/test/test_utils/test_plugin_menu.py index 9e77aa7d..6fea0912 100644 --- a/test/test_utils/test_plugin_menu.py +++ b/test/test_utils/test_plugin_menu.py @@ -48,7 +48,7 @@ def __init__(self) -> None: self.encoding = "utf-8" -@pytest.fixture() +@pytest.fixture def window(app): made = FakeWindow() yield made @@ -346,7 +346,7 @@ def mark_saved(self) -> None: self.events.append("saved") -@pytest.fixture() +@pytest.fixture def editor_tab(window, monkeypatch): """Put an editor tab in the window; the helper recognises it by type.""" monkeypatch.setattr(run_with, "EditorWidget", EditorTab) diff --git a/test/test_utils/test_prompt_store.py b/test/test_utils/test_prompt_store.py index 4d73ab92..b95cd4e6 100644 --- a/test/test_utils/test_prompt_store.py +++ b/test/test_utils/test_prompt_store.py @@ -20,7 +20,7 @@ CODE = "def f():\n pass\n" -@pytest.fixture() +@pytest.fixture def prompts(tmp_path, monkeypatch): """Point the prompt directory at a temporary one, never the real home.""" monkeypatch.setattr(prompt_store, "pybreeze_data_path", lambda: tmp_path) diff --git a/test/test_utils/test_prthinker_contract.py b/test/test_utils/test_prthinker_contract.py index af5ea679..5e4123c7 100644 --- a/test/test_utils/test_prthinker_contract.py +++ b/test/test_utils/test_prthinker_contract.py @@ -46,7 +46,7 @@ def _prthinker_python() -> str | None: reason="prthinker is not installed here (set PYBREEZE_PRTHINKER_PYTHON to run these)") -@pytest.fixture() +@pytest.fixture def closed_url() -> str: """An address on this machine where nothing listens.""" with socket.socket() as probe: diff --git a/test/test_utils/test_prthinker_menu_review.py b/test/test_utils/test_prthinker_menu_review.py index 8b8902ea..a5ea9021 100644 --- a/test/test_utils/test_prthinker_menu_review.py +++ b/test/test_utils/test_prthinker_menu_review.py @@ -15,7 +15,7 @@ class EditorTab(QWidget): """Stands in for a JEditor editor tab.""" -@pytest.fixture() +@pytest.fixture def window(monkeypatch): QApplication.instance() or QApplication([]) monkeypatch.setattr(menu, "EditorWidget", EditorTab) diff --git a/test/test_utils/test_prthinker_process.py b/test/test_utils/test_prthinker_process.py index 9f8e3b5c..da6261c8 100644 --- a/test/test_utils/test_prthinker_process.py +++ b/test/test_utils/test_prthinker_process.py @@ -61,7 +61,7 @@ def start_module_process(self, package, arguments, environment=None): RecordingProcess.calls.append((package, list(arguments), dict(environment or {}))) -@pytest.fixture() +@pytest.fixture def recorded(monkeypatch): """Catch the review before it reaches a real process or a real window.""" RecordingProcess.calls = [] @@ -72,7 +72,7 @@ def recorded(monkeypatch): return RecordingProcess.calls -@pytest.fixture() +@pytest.fixture def settings(monkeypatch): """Let each test decide what the settings hold.""" stored = dict(DEFAULT_SETTING) diff --git a/test/test_utils/test_prthinker_setting.py b/test/test_utils/test_prthinker_setting.py index 80f1058a..1a8e7684 100644 --- a/test/test_utils/test_prthinker_setting.py +++ b/test/test_utils/test_prthinker_setting.py @@ -15,7 +15,7 @@ ) -@pytest.fixture() +@pytest.fixture def data_dir(tmp_path, monkeypatch): """Keep every test's settings file inside its own temporary directory.""" monkeypatch.setattr(prthinker_setting, "pybreeze_data_path", lambda: tmp_path) diff --git a/test/test_utils/test_prthinker_setting_dialog.py b/test/test_utils/test_prthinker_setting_dialog.py index 2bfb7e03..73c646ec 100644 --- a/test/test_utils/test_prthinker_setting_dialog.py +++ b/test/test_utils/test_prthinker_setting_dialog.py @@ -27,14 +27,14 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def data_dir(tmp_path, monkeypatch): """Point the settings file at a temporary directory, never the real home.""" monkeypatch.setattr(prthinker_setting, "pybreeze_data_path", lambda: tmp_path) return tmp_path -@pytest.fixture() +@pytest.fixture def dialog(app, data_dir): made = PRThinkerSettingDialog() yield made diff --git a/test/test_utils/test_public_http.py b/test/test_utils/test_public_http.py index 08e17d78..ee3b34dd 100644 --- a/test/test_utils/test_public_http.py +++ b/test/test_utils/test_public_http.py @@ -35,7 +35,7 @@ def _answer(ip: str, port) -> list: return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, int(port or 0)))] -@pytest.fixture() +@pytest.fixture def dns(monkeypatch): """Names ending in ``.test`` answer from a table; ``rebind.test`` public first, then loopback.""" rebind_answers = [_PUBLIC_IP] @@ -51,7 +51,7 @@ def getaddrinfo(host, port=None, *args, **kwargs): monkeypatch.setattr(socket, "getaddrinfo", getaddrinfo) -@pytest.fixture() +@pytest.fixture def loopback_allowed(monkeypatch): """Let the checks pass loopback, so a local server can stand in for a public one.""" monkeypatch.setattr(url_validation, "_is_blocked_ip", lambda _ip: False) @@ -110,7 +110,7 @@ def _session() -> requests.Session: return session -@pytest.fixture() +@pytest.fixture def listener(): server = _Listener() yield server @@ -216,7 +216,7 @@ def test_https_through_a_proxy_is_left_to_the_proxy(self, dns, listener): assert listener.received[0].startswith(b"CONNECT rebind.test:443 HTTP/1.") -@pytest.fixture() +@pytest.fixture def two_addresses(monkeypatch): """``two.test`` answers an address nothing listens on first, then loopback.""" def getaddrinfo(host, port=None, *args, **kwargs): diff --git a/test/test_utils/test_query_json_gui.py b/test/test_utils/test_query_json_gui.py index 5406f48d..a111e9b7 100644 --- a/test/test_utils/test_query_json_gui.py +++ b/test/test_utils/test_query_json_gui.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.query_json_gui import QueryJsonGUI gui = QueryJsonGUI() diff --git a/test/test_utils/test_regex_gui.py b/test/test_utils/test_regex_gui.py index e49aaf7d..f396feff 100644 --- a/test/test_utils/test_regex_gui.py +++ b/test/test_utils/test_regex_gui.py @@ -26,7 +26,7 @@ def run(widget) -> None: QApplication.processEvents() -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.regex_gui import RegexGUI gui = RegexGUI() diff --git a/test/test_utils/test_response_inspector_gui.py b/test/test_utils/test_response_inspector_gui.py index 1ba8ad2d..2dab883f 100644 --- a/test/test_utils/test_response_inspector_gui.py +++ b/test/test_utils/test_response_inspector_gui.py @@ -27,7 +27,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.response_inspector_gui import ResponseInspectorGUI gui = ResponseInspectorGUI() @@ -94,7 +94,7 @@ def __init__(self): self.tab_widget = _FakeTabWidget() -@pytest.fixture() +@pytest.fixture def widget_with_window(app): from pybreeze.pybreeze_ui.tools_gui.response_inspector_gui import ResponseInspectorGUI window = _FakeMainWindow() diff --git a/test/test_utils/test_run_folder.py b/test/test_utils/test_run_folder.py index 6d7d02be..96f84feb 100644 --- a/test/test_utils/test_run_folder.py +++ b/test/test_utils/test_run_folder.py @@ -21,7 +21,7 @@ def window(): made.deleteLater() -@pytest.fixture() +@pytest.fixture def picked(monkeypatch): """The folder the dialog returns, and the parents it was opened with.""" state = {"folder": "", "parents": [], "told": []} diff --git a/test/test_utils/test_run_window_title.py b/test/test_utils/test_run_window_title.py index 20f56bdc..a135d3a5 100644 --- a/test/test_utils/test_run_window_title.py +++ b/test/test_utils/test_run_window_title.py @@ -25,7 +25,7 @@ def app(): return QApplication.instance() or QApplication([]) -@pytest.fixture() +@pytest.fixture def manager(app): from pybreeze.extend.process_executor.python_task_process_manager import TaskProcessManager from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow diff --git a/test/test_utils/test_sftp_tree_actions.py b/test/test_utils/test_sftp_tree_actions.py index 57cb9d28..bfbe0257 100644 --- a/test/test_utils/test_sftp_tree_actions.py +++ b/test/test_utils/test_sftp_tree_actions.py @@ -66,7 +66,7 @@ def close(self) -> None: """Nothing to close.""" -@pytest.fixture() +@pytest.fixture def tree(app, monkeypatch): client = FakeClient({ "/": [_entry("src", directory=True), _entry("notes.txt")], diff --git a/test/test_utils/test_sftp_upload.py b/test/test_utils/test_sftp_upload.py index 20583165..9f24a978 100644 --- a/test/test_utils/test_sftp_upload.py +++ b/test/test_utils/test_sftp_upload.py @@ -75,7 +75,7 @@ def _wrapper(sftp: FakeSftp) -> sftp_session.SFTPClientWrapper: return wrapper -@pytest.fixture() +@pytest.fixture def local_file(tmp_path): path = tmp_path / "config.yaml" path.write_bytes(b"new config") diff --git a/test/test_utils/test_skills_panel.py b/test/test_utils/test_skills_panel.py index e9b28f16..416ada4f 100644 --- a/test/test_utils/test_skills_panel.py +++ b/test/test_utils/test_skills_panel.py @@ -29,7 +29,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def panel(tmp_path, monkeypatch): # The built-in prompts, never the user's own monkeypatch.setattr(prompt_store, "pybreeze_data_path", lambda: tmp_path) @@ -38,7 +38,7 @@ def panel(tmp_path, monkeypatch): widget.deleteLater() -@pytest.fixture() +@pytest.fixture def answer(monkeypatch): """What the replace question gets, and how often it was asked.""" state = {"reply": QMessageBox.StandardButton.No, "asked": 0} diff --git a/test/test_utils/test_ssh_host_key_policy.py b/test/test_utils/test_ssh_host_key_policy.py index 6a6086f7..04066842 100644 --- a/test/test_utils/test_ssh_host_key_policy.py +++ b/test/test_utils/test_ssh_host_key_policy.py @@ -27,7 +27,7 @@ def keys(): return paramiko.RSAKey.generate(1024), paramiko.RSAKey.generate(1024) -@pytest.fixture() +@pytest.fixture def asked(app, tmp_path, monkeypatch): """Answers every question with ``asked["answer"]`` and counts them.""" state = {"answer": True, "count": 0} diff --git a/test/test_utils/test_ssh_login_form.py b/test/test_utils/test_ssh_login_form.py index ccf19e89..678f909a 100644 --- a/test/test_utils/test_ssh_login_form.py +++ b/test/test_utils/test_ssh_login_form.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def form(app): widget = login_mod.LoginWidget() yield widget diff --git a/test/test_utils/test_test_pioneer_template.py b/test/test_utils/test_test_pioneer_template.py index f4b0a1df..1c44a23a 100644 --- a/test/test_utils/test_test_pioneer_template.py +++ b/test/test_utils/test_test_pioneer_template.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def window(app, tmp_path, monkeypatch): made = QMainWindow() made.working_dir = str(tmp_path / "project") diff --git a/test/test_utils/test_testpioneer_menu.py b/test/test_utils/test_testpioneer_menu.py index fab4d028..2ae2344e 100644 --- a/test/test_utils/test_testpioneer_menu.py +++ b/test/test_utils/test_testpioneer_menu.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def chosen(monkeypatch) -> dict: """Answer the file dialog with ``chosen["path"]``; record the filter and what ran.""" state: dict = {"path": "", "ran": [], "filter": None, "told": 0} diff --git a/test/test_utils/test_timestamp_gui.py b/test/test_utils/test_timestamp_gui.py index 4fb4ec3a..412ee037 100644 --- a/test/test_utils/test_timestamp_gui.py +++ b/test/test_utils/test_timestamp_gui.py @@ -18,7 +18,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.timestamp_gui import TimestampGUI gui = TimestampGUI() diff --git a/test/test_utils/test_tool_output_wording.py b/test/test_utils/test_tool_output_wording.py index de89f6b9..4d102aeb 100644 --- a/test/test_utils/test_tool_output_wording.py +++ b/test/test_utils/test_tool_output_wording.py @@ -27,7 +27,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def chinese(app, monkeypatch): from je_editor import language_wrapper @@ -35,7 +35,7 @@ def chinese(app, monkeypatch): monkeypatch.setattr(language_wrapper, "language_word_dict", pybreeze_traditional_chinese_word_dict) -@pytest.fixture() +@pytest.fixture def english(app, monkeypatch): from je_editor import language_wrapper diff --git a/test/test_utils/test_unsaved_on_close.py b/test/test_utils/test_unsaved_on_close.py index 1a16486d..08f07099 100644 --- a/test/test_utils/test_unsaved_on_close.py +++ b/test/test_utils/test_unsaved_on_close.py @@ -22,7 +22,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def answers(monkeypatch): """Answer every question with ``answers["reply"]`` and count them.""" state = {"reply": QMessageBox.StandardButton.No, "asked": 0} diff --git a/test/test_utils/test_url_builder_gui.py b/test/test_utils/test_url_builder_gui.py index 90ef9d0a..831848bd 100644 --- a/test/test_utils/test_url_builder_gui.py +++ b/test/test_utils/test_url_builder_gui.py @@ -19,7 +19,7 @@ def app(): return instance -@pytest.fixture() +@pytest.fixture def widget(app): from pybreeze.pybreeze_ui.tools_gui.url_builder_gui import UrlBuilderGUI gui = UrlBuilderGUI() diff --git a/test/test_utils/test_url_parser_differential.py b/test/test_utils/test_url_parser_differential.py index a757a7a3..54d23858 100644 --- a/test/test_utils/test_url_parser_differential.py +++ b/test/test_utils/test_url_parser_differential.py @@ -18,7 +18,7 @@ _PUBLIC = [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.215.14", 0))] -@pytest.fixture() +@pytest.fixture def public_dns(monkeypatch): """Every name resolves to a public address, so only the parsing decides.""" monkeypatch.setattr(url_validation.socket, "getaddrinfo", lambda *_a, **_k: _PUBLIC) From c330be78f5e5fc7ba7432782900686f8e69e7ea8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:27:59 +0800 Subject: [PATCH 140/166] Resize a node and an image through one function --- architecture_explore.md | 6 +- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- .../diagram_editor/diagram_items.py | 87 +++++++++---------- test/test_utils/test_diagram_editing.py | 40 +++++++++ 5 files changed, 93 insertions(+), 51 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 502a7f6d..d2ed65db 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -243,13 +243,13 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) --- -## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,062 行,最大子系統) +## 7. `pybreeze_ui/diagram_editor/` — 架構圖編輯器(4,055 行,最大子系統) | 檔案 | 職責 | |---|---| | `diagram_editor_widget.py` (695) | 外層 widget:兩排工具列(工具模式列 + 檔案/undo/對齊/格線/匯出/縮放列)、canvas 與屬性面板的 splitter、快捷鍵(只在編輯器有焦點時作用,當 dock 開著也不搶程式碼編輯器的按鍵);PNG/SVG 匯出;Mermaid 匯入對話框。「從 URL 加入圖片」也交給 `ImageDownloadThread`,圖片回來才放上畫布,失敗或不是圖片就跳警告;關閉時還在跑的下載交給 `let_run_out()`。存檔經 `replace_text()` 先寫 `.saving` 再換上去,存檔失敗不會毀掉上一份 | | `diagram_scene.py` (915) | `DiagramScene(QGraphicsScene)`:**State pattern** 的 `ToolMode` 決定滑鼠行為;undo/redo、複製貼上(節點、連線與圖片)、多選對齊與分佈、z-order、序列化 `to_dict()` / `load_from_dict()`。`get_all_nodes/connections/images()` 由下往上列出(`_bottom_first()`),存檔與 undo 還原時同 z 值的重疊項目維持原本的上下;右鍵選單先選取點到的項目;置頂/置底放到所有其他節點與圖片之上/之下;`to_dict()` 給每個節點與圖片記下它在兩者之間由下往上的位置(`stack`),載入後 `_restore_stacking()` 照這個順序重新加回場景(同 z 值時後加的在上面),圖片也存 `z`。`load_from_dict()` 先用 `_check_is_a_diagram()` 確認資料形狀才清空畫布(不合就丟 `ValueError`,畫布原封不動),每一筆節點/連線/圖片再各自容錯;清空前先 `to_dict()` 留一份,載入途中還是出錯就放回原樣再往上丟——載入要嘛成功、要嘛什麼都沒變(編輯器存檔寫回上次開的檔案,半途清空的畫布會蓋掉使用者的檔)。圖片的 `source` 不是字串就丟掉。`undo_scope` 用 `try/finally`,本體丟例外也一定收掉快照;圖片來源先看副檔名、拒絕 UNC(`_is_on_this_machine()`)才碰檔案系統;URL 圖片交給 `ImageDownloadThread(QThread)` 下載並快取在 `_pixmap_cache`,undo/redo 重建項目時直接用快取,不會再連一次網路;編輯器關閉時 `let_image_downloads_run_out()` 把還在跑的下載交給 `let_run_out()`,不在 UI 執行緒等 | -| `diagram_items.py` (969) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | +| `diagram_items.py` (962) | 圖元:`DiagramNode`(矩形/圓角/橢圓/菱形 4 種 body + 置中標籤 + 4 個 `ResizeHandle`;填色、框線色、字級收在 frozen dataclass `NodeStyle`)、`DiagramConnection`(三次貝茲 + 箭頭,連到節點邊界交點)、`DiagramImage`。`_EditableLabel` 刻意預設唯讀、雙擊才進編輯(對應 CLAUDE.md 的 Qt 規範);雙擊時記下場景快照,失去焦點時經 `DiagramScene.record_change()` 記成一步「Edit Text」undo(有改才記)。`DiagramScene.add_image()`(`diagram_scene.py`)把圖放進 scene 的 `_pixmap_cache`,undo 重建時不必重讀檔案或重新下載。檔案裡的字級經 `_clamped_font_size()`:不是有限數字(`1e999` 讀進來是無限大、NaN、字串)就用預設字級;位置經 `_coordinate()`:不是有限數字就跳過這一筆,超過 `MAX_COORDINATE`(一百萬)就夾回來;連線建好所有東西之後才掛到兩端節點上 | | `diagram_mermaid_parser.py` (629) | Mermaid flowchart → diagram dict。切箭頭與 `;` 之前先用 `_protect()` 把引號與括號裡的標籤換成佔位符,解析節點時再 `_restore()`(標籤裡的 `-->`、`;` 不會被當成語法)。箭頭的 `|label|` 由 `_arrow_label()` 用兩個相鄰部分不共用字元的樣式讀(引號標籤一個、一般標籤一個,取最左邊的),線性時間:原本合成一個、標籤兩側可有空白的樣式在未閉合的標籤上是立方時間回溯。含 **Sugiyama 風格自動排版**:分層 → 交叉最小化掃描 → 交叉軸偏移解析 | | `diagram_property_panel.py` (453) | 右側屬性側欄,依選取型別切換 node / connection / image 三組表單;每記一步 undo(場景的 `recorded`)就重新整理(在畫布上拖把手改大小時選取沒變);寬、高各自只改自己那一邊;數字欄位不追鍵盤、輸入完才套用 | | `diagram_view.py` (228) | `QGraphicsView`:滾輪與按鈕縮放都經 `_step_zoom()`(有上下界,界外時仍可往界內走;橫向滾輪不縮放),`fit()` 把「符合視窗」夾在上下界內、中鍵或右鍵拖曳平移(右鍵拖過畫布就不開右鍵選單,`contextMenuEvent`;放開沒送到它時下一次移動就停止平移)、`drawBackground` 畫格線 | @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2621 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2635 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 76f08c32..e375feef 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -745,3 +745,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: 51 files in `test/test_utils/` - **Checks**: `test/test_utils` still collects 2621 tests; a sample of files using these fixtures passes (173). The other test-code issues (composite asserts, S9073) are left: splitting 78 asserts by hand is churn for a clearer failure message only. - **Open items**: none. + +## U-20260925-89 · 2026-09-25 · A node and an image resize through one function · #refactor #diagram + +- **What**: refactor, no change in behaviour. SonarCloud's only duplication in the project (0.1 %) was the resize arithmetic `DiagramNode._apply_resize` and `DiagramImage._apply_resize` each spelled out (16 and 14 lines), differing only in the minimum height; CLAUDE.md asks for a helper once a block of six lines or more repeats. +- **Change**: `resized_geometry(role, delta, orig_rect, orig_pos, min_w, min_h)` in `diagram_items.py` gives the new position and size; the node calls it with `_MIN_NODE_W` / `_MIN_NODE_H` (40 x 20) and the image with `_MIN_IMAGE_SIDE` (40 x 40), the constants the constructors already used. +- **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_items.py`, `test/test_utils/test_diagram_editing.py`, `architecture_explore.md` (line and test counts) +- **Checks**: `TestResizeFromAHandle` (fourteen drags of a node and an image, every side, both clamps, the far side staying put) was written first and passed before the change as after it; the diagram editing, editor, property panel and image tests pass (79). `test/test_utils` collects 2635 tests; `ruff` clean. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b5004d43..5376454e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-89 | 2026-09-25 | A node and an image resize through one function | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-88 | 2026-09-25 | Fixture decorators without empty parentheses | #refactor #test | [2026-09-c](2026-09-c.md) | | U-20260925-87 | 2026-09-25 | SonarCloud's open issues in pybreeze/ dealt with | #refactor #quality | [2026-09-c](2026-09-c.md) | | U-20260925-86 | 2026-09-25 | An arrow's label is read in linear time | #fix #diagram | [2026-09-c](2026-09-c.md) | @@ -491,4 +492,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 88 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 89 | diff --git a/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py b/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py index 7e05039a..dcec99a3 100644 --- a/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py +++ b/pybreeze/pybreeze_ui/diagram_editor/diagram_items.py @@ -168,6 +168,41 @@ def _number(value: object, fallback: _Fallback) -> float | _Fallback: return number if math.isfinite(number) else fallback +def resized_geometry(role: str, delta: QPointF, orig_rect: QRectF, orig_pos: QPointF, + min_w: float, min_h: float) -> tuple[float, float, float, float]: + """Where an item dragged by its resize handle goes: ``(x, y, width, height)``. + + :param role: the handle's sides, any of ``"l"``, ``"r"``, ``"t"``, ``"b"``; a left or top + handle moves the item as it resizes it + :param delta: how far the handle has been dragged + :param orig_rect: the item's rect when the drag started + :param orig_pos: the item's position when the drag started + :param min_w: the narrowest the item may become; the far side stays where it was + :param min_h: the lowest the item may become; the far side stays where it was + """ + new_x, new_y = orig_pos.x(), orig_pos.y() + new_w, new_h = orig_rect.width(), orig_rect.height() + if "r" in role: + new_w = orig_rect.width() + delta.x() + if "l" in role: + new_w = orig_rect.width() - delta.x() + new_x = orig_pos.x() + delta.x() + if "b" in role: + new_h = orig_rect.height() + delta.y() + if "t" in role: + new_h = orig_rect.height() - delta.y() + new_y = orig_pos.y() + delta.y() + if new_w < min_w: + if "l" in role: + new_x = orig_pos.x() + orig_rect.width() - min_w + new_w = min_w + if new_h < min_h: + if "t" in role: + new_y = orig_pos.y() + orig_rect.height() - min_h + new_h = min_h + return new_x, new_y, new_w, new_h + + # How far from the origin a saved item may be placed. A file is anyone's to # edit: NaN (which json.loads accepts) made an item invisible and the scene's # bounding rect NaN, so every export failed; 1e308 overflowed the export size. @@ -416,32 +451,8 @@ def set_size(self, w: float, h: float) -> None: conn.update_path() def _apply_resize(self, role: str, delta: QPointF, orig_rect: QRectF, orig_pos: QPointF) -> None: - new_w = orig_rect.width() - new_h = orig_rect.height() - new_x = orig_pos.x() - new_y = orig_pos.y() - - if "r" in role: - new_w = orig_rect.width() + delta.x() - if "l" in role: - new_w = orig_rect.width() - delta.x() - new_x = orig_pos.x() + delta.x() - if "b" in role: - new_h = orig_rect.height() + delta.y() - if "t" in role: - new_h = orig_rect.height() - delta.y() - new_y = orig_pos.y() + delta.y() - - # Clamp minimum - if new_w < 40: - if "l" in role: - new_x = orig_pos.x() + orig_rect.width() - 40 - new_w = 40 - if new_h < 20: - if "t" in role: - new_y = orig_pos.y() + orig_rect.height() - 20 - new_h = 20 - + new_x, new_y, new_w, new_h = resized_geometry( + role, delta, orig_rect, orig_pos, _MIN_NODE_W, _MIN_NODE_H) self.setPos(new_x, new_y) self.prepareGeometryChange() self.node_w = new_w @@ -872,27 +883,9 @@ def set_size(self, w: float, h: float) -> None: self._center_label() self._update_handles() - def _apply_resize(self, role, delta, orig_rect, orig_pos): - new_w, new_h = orig_rect.width(), orig_rect.height() - new_x, new_y = orig_pos.x(), orig_pos.y() - if "r" in role: - new_w = orig_rect.width() + delta.x() - if "l" in role: - new_w = orig_rect.width() - delta.x() - new_x = orig_pos.x() + delta.x() - if "b" in role: - new_h = orig_rect.height() + delta.y() - if "t" in role: - new_h = orig_rect.height() - delta.y() - new_y = orig_pos.y() + delta.y() - if new_w < 40: - if "l" in role: - new_x = orig_pos.x() + orig_rect.width() - 40 - new_w = 40 - if new_h < 40: - if "t" in role: - new_y = orig_pos.y() + orig_rect.height() - 40 - new_h = 40 + def _apply_resize(self, role: str, delta: QPointF, orig_rect: QRectF, orig_pos: QPointF) -> None: + new_x, new_y, new_w, new_h = resized_geometry( + role, delta, orig_rect, orig_pos, _MIN_IMAGE_SIDE, _MIN_IMAGE_SIDE) self.setPos(new_x, new_y) self.set_size(new_w, new_h) diff --git a/test/test_utils/test_diagram_editing.py b/test/test_utils/test_diagram_editing.py index 43ea2bff..09ce91e8 100644 --- a/test/test_utils/test_diagram_editing.py +++ b/test/test_utils/test_diagram_editing.py @@ -316,3 +316,43 @@ def test_is_in_the_ide_language(self, app, monkeypatch, add, expected): (node,) = [item for item in scene.items() if isinstance(item, DiagramNode)] assert node.text() == expected + + +class TestResizeFromAHandle: + """Where a handle drag leaves a node or an image: the size follows the handle's + sides, a left or top handle moves the item too, and each keeps a minimum size + (a node 40 x 20, an image 40 x 40) with its far side where it was.""" + + @pytest.mark.parametrize("role,dx,dy,expected", [ + ("r", 60, 0, (10, 10, 200, 60)), + ("b", 0, 30, (10, 10, 140, 90)), + ("l", 20, 0, (30, 10, 120, 60)), + ("t", 0, 20, (10, 30, 140, 40)), + ("br", 10, 10, (10, 10, 150, 70)), + ("tl", -10, -10, (0, 0, 150, 70)), + ("r", -500, 0, (10, 10, 40, 60)), + ("l", 500, 0, (110, 10, 40, 60)), + ("b", 0, -500, (10, 10, 140, 20)), + ("t", 0, 500, (10, 50, 140, 20)), + ]) + def test_a_node(self, app, role, dx, dy, expected): + from PySide6.QtCore import QRectF + + node = DiagramNode(x=10, y=10, w=140, h=60, text="A") + node._apply_resize(role, QPointF(dx, dy), QRectF(0, 0, 140, 60), QPointF(10, 10)) + + assert (node.pos().x(), node.pos().y(), node.node_w, node.node_h) == expected + + @pytest.mark.parametrize("role,dx,dy,expected", [ + ("r", 60, 0, (10, 10, 200, 60)), + ("t", 0, 500, (10, 30, 140, 40)), + ("b", 0, -500, (10, 10, 140, 40)), + ("l", 500, 0, (110, 10, 40, 60)), + ]) + def test_an_image(self, app, role, dx, dy, expected): + from PySide6.QtCore import QRectF + + image = DiagramImage(x=10, y=10, w=140, h=60) + image._apply_resize(role, QPointF(dx, dy), QRectF(0, 0, 140, 60), QPointF(10, 10)) + + assert (image.pos().x(), image.pos().y(), image.img_w, image.img_h) == expected From 8c10e604bf9d529eac12cd7c78072b12421ce36b Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:32:45 +0800 Subject: [PATCH 141/166] Test the diagram canvas's zoom and grid --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- .../test_utils/test_diagram_view_zoom_grid.py | 96 +++++++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_diagram_view_zoom_grid.py diff --git a/architecture_explore.md b/architecture_explore.md index d2ed65db..dea5518c 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 145 個 `test_*.py`、2635 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 146 個 `test_*.py`、2644 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index e375feef..ce19df75 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -753,3 +753,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/diagram_editor/diagram_items.py`, `test/test_utils/test_diagram_editing.py`, `architecture_explore.md` (line and test counts) - **Checks**: `TestResizeFromAHandle` (fourteen drags of a node and an image, every side, both clamps, the far side staying put) was written first and passed before the change as after it; the diagram editing, editor, property panel and image tests pass (79). `test/test_utils` collects 2635 tests; `ruff` clean. - **Open items**: none. + +## U-20260925-90 · 2026-09-25 · The diagram canvas's zoom and grid are tested · #test #diagram + +- **What**: SonarCloud's coverage by file put `diagram_view.py` at 47.7 % on `main` (71 % from the diagram tests run locally): nothing tested Ctrl+0's `set_zoom`, the grid's properties, or the background it draws when Grid is on. +- **Change**: `test_diagram_view_zoom_grid.py`: `set_zoom` sets (not multiplies) the scale, keeps it within 10 %–500 % and reports the percentage; the grid is off until turned on and a cell is never under 5; the background drawn into an image is plain without the grid, and with it has a major line every fifth cell and minor lines on multiples of the cell size, left of the origin too (Python's modulo of a negative coordinate). Fit's clamping was already tested in `test_diagram_editing.py`. +- **Files**: `test/test_utils/test_diagram_view_zoom_grid.py` (new), `architecture_explore.md` (test count) +- **Checks**: the nine tests pass; the diagram tests now cover 97 % of `diagram_view.py` (the rest: a grid-size repaint while the grid is on, and one branch each of `fit` and `mousePressEvent`). `test/test_utils` collects 2644 tests in 146 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 5376454e..6d25ad90 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-90 | 2026-09-25 | The diagram canvas's zoom and grid are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-89 | 2026-09-25 | A node and an image resize through one function | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-88 | 2026-09-25 | Fixture decorators without empty parentheses | #refactor #test | [2026-09-c](2026-09-c.md) | | U-20260925-87 | 2026-09-25 | SonarCloud's open issues in pybreeze/ dealt with | #refactor #quality | [2026-09-c](2026-09-c.md) | @@ -492,4 +493,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 89 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 90 | diff --git a/test/test_utils/test_diagram_view_zoom_grid.py b/test/test_utils/test_diagram_view_zoom_grid.py new file mode 100644 index 00000000..581efdce --- /dev/null +++ b/test/test_utils/test_diagram_view_zoom_grid.py @@ -0,0 +1,96 @@ +"""The diagram canvas's zoom (Ctrl+0) and the background grid it draws. + +Fit is covered by test_diagram_editing.py. +""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtCore import QRectF +from PySide6.QtGui import QImage, QPainter +from PySide6.QtWidgets import QApplication + +from pybreeze.pybreeze_ui.diagram_editor import diagram_view as view_module +from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene +from pybreeze.pybreeze_ui.diagram_editor.diagram_view import DiagramView + + +@pytest.fixture(scope="module") +def app(): + return QApplication.instance() or QApplication([]) + + +@pytest.fixture +def view(app): + made = DiagramView(DiagramScene()) + made.resize(400, 300) + yield made + made.deleteLater() + + +def _zoom_reported(view) -> list[int]: + reported: list[int] = [] + view.zoom_changed.connect(reported.append) + return reported + + +class TestSetZoom: + @pytest.mark.parametrize("percent,scale", [(100, 1.0), (250, 2.5), (1, 0.1), (900, 5.0)]) + def test_the_zoom_is_set_within_the_range(self, view, percent, scale): + reported = _zoom_reported(view) + view.set_zoom(150) # from somewhere else, not added to it + + view.set_zoom(percent) + + assert view.transform().m11() == pytest.approx(scale) + assert reported[-1] == int(scale * 100) + + +class TestTheGrid: + def test_it_is_off_until_turned_on(self, view): + assert view.draw_grid is False + view.draw_grid = True + assert view.draw_grid is True + + def test_a_cell_is_never_smaller_than_five(self, view): + view.grid_size = 2 + assert view.grid_size == 5 + view.grid_size = 30 + assert view.grid_size == 30 + + @staticmethod + def _background(view, left: float, top: float) -> QImage: + """The background drawn for the 100 x 100 scene area at (*left*, *top*), as an image.""" + image = QImage(100, 100, QImage.Format.Format_RGB32) + painter = QPainter(image) + painter.translate(-left, -top) + view.drawBackground(painter, QRectF(left, top, 100, 100)) + painter.end() + return image + + def test_without_it_the_background_is_plain(self, view): + image = self._background(view, 0, 0) + + assert {image.pixelColor(x, 50).name() for x in range(100)} == {view_module._BG_COLOR.name()} + + def test_lines_fall_on_multiples_of_the_cell_size(self, view): + view.draw_grid = True + image = self._background(view, 0, 0) + + background = view_module._BG_COLOR.name() + assert image.pixelColor(0, 50).name() == view_module._GRID_COLOR_MAJOR.name() # every fifth line + assert image.pixelColor(20, 50).name() != background # a minor line, drawn thinner + assert image.pixelColor(10, 50).name() == background + + def test_left_of_the_origin_too(self, view): + # Scene x -30 to 70: lines at -20, 0, 20..., which are image columns 10, 30, 50... + view.draw_grid = True + image = self._background(view, -30, 0) + + background = view_module._BG_COLOR.name() + assert image.pixelColor(10, 50).name() != background + assert image.pixelColor(30, 50).name() == view_module._GRID_COLOR_MAJOR.name() + assert image.pixelColor(20, 50).name() == background From 1a0dddd8ad8eae2dc86c99bc30d109e3b469e5d0 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:37:52 +0800 Subject: [PATCH 142/166] Test the diagram canvas's clicks and keys --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 + docs/updates/README.md | 3 +- test/test_utils/test_diagram_scene_actions.py | 192 ++++++++++++++++++ 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 test/test_utils/test_diagram_scene_actions.py diff --git a/architecture_explore.md b/architecture_explore.md index dea5518c..549b7868 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 146 個 `test_*.py`、2644 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2655 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ce19df75..1486b937 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -761,3 +761,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_diagram_view_zoom_grid.py` (new), `architecture_explore.md` (test count) - **Checks**: the nine tests pass; the diagram tests now cover 97 % of `diagram_view.py` (the rest: a grid-size repaint while the grid is on, and one branch each of `fit` and `mousePressEvent`). `test/test_utils` collects 2644 tests in 146 files. - **Open items**: none. + +## U-20260925-91 · 2026-09-25 · The diagram canvas's clicks and keys are tested · #test #diagram + +- **What**: `diagram_scene.py` had 68.8 % coverage on `main` (83 % from the diagram tests locally). Untested were drawing a connection by clicking, cancelling one, the Delete / Backspace / Esc keys, deleting a selection, Select All and the main path of vertical distribution. +- **Change**: `test_diagram_scene_actions.py`: a click on the source and one on the target make one connection, one undo step, with the dashed line gone and the tool back to Select; a click on the empty canvas, on the source itself or Esc cancels it; Delete and Backspace take a node and its connections in one undo step, a connection alone leaves its nodes, and nothing selected leaves no undo step; while a node's text is being edited, Delete edits the text instead of removing the node; Select All takes nodes, connections and images but not a connection being drawn; vertical distribution leaves equal gaps between the outer two and moves nothing sideways. +- **Files**: `test/test_utils/test_diagram_scene_actions.py` (new), `architecture_explore.md` (test count) +- **Checks**: the eleven tests pass as the code stands (no behaviour needed changing); the diagram tests now cover 91 % of `diagram_scene.py`. `test/test_utils` collects 2655 tests in 147 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6d25ad90..0f28d0d7 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-91 | 2026-09-25 | The diagram canvas's clicks and keys are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-90 | 2026-09-25 | The diagram canvas's zoom and grid are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-89 | 2026-09-25 | A node and an image resize through one function | #refactor #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-88 | 2026-09-25 | Fixture decorators without empty parentheses | #refactor #test | [2026-09-c](2026-09-c.md) | @@ -493,4 +494,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 90 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 91 | diff --git a/test/test_utils/test_diagram_scene_actions.py b/test/test_utils/test_diagram_scene_actions.py new file mode 100644 index 00000000..8afffdc1 --- /dev/null +++ b/test/test_utils/test_diagram_scene_actions.py @@ -0,0 +1,192 @@ +"""What the diagram canvas does with clicks and keys: drawing a connection, Delete, Esc, Select All. + +The align commands and horizontal distribution are in test_diagram_align.py. +""" +from __future__ import annotations + +import os + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +import pytest +from PySide6.QtCore import QEvent, QPointF, Qt +from PySide6.QtGui import QKeyEvent +from PySide6.QtWidgets import QApplication, QGraphicsLineItem, QGraphicsSceneMouseEvent + +from pybreeze.extend_multi_language.update_language_dict import update_language_dict +from pybreeze.pybreeze_ui.diagram_editor.diagram_items import DiagramConnection, DiagramImage, DiagramNode +from pybreeze.pybreeze_ui.diagram_editor.diagram_scene import DiagramScene, ToolMode + + +@pytest.fixture(scope="module") +def app(): + instance = QApplication.instance() or QApplication([]) + update_language_dict() + return instance + + +@pytest.fixture +def scene(app): + diagram = DiagramScene() + yield diagram + diagram.deleteLater() + + +def _two_nodes(scene: DiagramScene) -> tuple[DiagramNode, DiagramNode]: + first = DiagramNode(x=0, y=0, w=100, h=60, text="A") + second = DiagramNode(x=300, y=0, w=100, h=60, text="B") + scene.addItem(first) + scene.addItem(second) + return first, second + + +def _click(scene: DiagramScene, x: float, y: float) -> None: + press = QGraphicsSceneMouseEvent(QEvent.Type.GraphicsSceneMousePress) + press.setScenePos(QPointF(x, y)) + press.setButton(Qt.MouseButton.LeftButton) + press.setButtons(Qt.MouseButton.LeftButton) + scene.mousePressEvent(press) + + +def _press(scene: DiagramScene, key: Qt.Key) -> None: + scene.keyPressEvent(QKeyEvent(QEvent.Type.KeyPress, key, Qt.KeyboardModifier.NoModifier)) + + +def _dashed_lines(scene: DiagramScene) -> list: + return [item for item in scene.items() if isinstance(item, QGraphicsLineItem)] + + +class TestDrawingAConnection: + def test_the_source_then_the_target_makes_one_undoable_connection(self, scene): + first, second = _two_nodes(scene) + scene.mode = ToolMode.ADD_CONNECTION + + _click(scene, 50, 30) + assert len(_dashed_lines(scene)) == 1 # the line that follows the mouse + _click(scene, 350, 30) + + (connection,) = scene.get_all_connections() + assert (connection.source, connection.target) == (first, second) + assert _dashed_lines(scene) == [] + assert scene.mode == ToolMode.SELECT + scene.undo_stack.undo() + assert scene.get_all_connections() == [] + + def test_a_click_on_the_empty_canvas_cancels_it(self, scene): + _two_nodes(scene) + scene.mode = ToolMode.ADD_CONNECTION + + _click(scene, 50, 30) + _click(scene, 200, 300) + + assert scene.get_all_connections() == [] + assert _dashed_lines(scene) == [] + + def test_a_node_is_not_connected_to_itself(self, scene): + _two_nodes(scene) + scene.mode = ToolMode.ADD_CONNECTION + + _click(scene, 50, 30) + _click(scene, 60, 40) + + assert scene.get_all_connections() == [] + assert scene.mode == ToolMode.SELECT + + def test_esc_cancels_it_and_goes_back_to_select(self, scene): + _two_nodes(scene) + scene.mode = ToolMode.ADD_CONNECTION + _click(scene, 50, 30) + + _press(scene, Qt.Key.Key_Escape) + _click(scene, 350, 30) + + assert scene.get_all_connections() == [] + assert _dashed_lines(scene) == [] + assert scene.mode == ToolMode.SELECT + + +class TestDelete: + @pytest.mark.parametrize("key", [Qt.Key.Key_Delete, Qt.Key.Key_Backspace]) + def test_a_node_goes_with_its_connections_in_one_undo_step(self, scene, key): + first, second = _two_nodes(scene) + with scene.undo_scope("Connect"): + scene.addItem(DiagramConnection(first, second)) + first.setSelected(True) + + _press(scene, key) + + assert [node.text() for node in scene.get_all_nodes()] == ["B"] + assert scene.get_all_connections() == [] + scene.undo_stack.undo() + assert sorted(node.text() for node in scene.get_all_nodes()) == ["A", "B"] + assert len(scene.get_all_connections()) == 1 + + def test_a_connection_alone_leaves_its_nodes(self, scene): + first, second = _two_nodes(scene) + connection = DiagramConnection(first, second) + scene.addItem(connection) + connection.setSelected(True) + + scene.delete_selected() + + assert scene.get_all_connections() == [] + assert len(scene.get_all_nodes()) == 2 + assert first.connections == [] and second.connections == [] + + def test_nothing_selected_leaves_no_undo_step(self, scene): + _two_nodes(scene) + + scene.delete_selected() + + assert not scene.undo_stack.canUndo() + + def test_while_a_node_is_being_edited_the_key_edits_its_text(self, app): + # Delete removed the node being typed into + from PySide6.QtWidgets import QGraphicsView + + scene = DiagramScene() + view = QGraphicsView(scene) + view.show() + node, _second = _two_nodes(scene) + node.setSelected(True) + node.label.setTextInteractionFlags(Qt.TextInteractionFlag.TextEditorInteraction) + node.label.setFocus() + app.processEvents() + + _press(scene, Qt.Key.Key_Delete) + + assert node in scene.get_all_nodes() + view.close() + view.deleteLater() + + +class TestSelectAll: + def test_every_node_connection_and_image_but_not_the_dashed_line(self, scene): + first, second = _two_nodes(scene) + scene.addItem(DiagramConnection(first, second)) + scene.addItem(DiagramImage(x=0, y=200, w=50, h=50)) + scene.mode = ToolMode.ADD_CONNECTION + _click(scene, 50, 30) # a connection being drawn + + scene.select_all() + + assert len(scene.selectedItems()) == 4 + assert not any(isinstance(item, QGraphicsLineItem) for item in scene.selectedItems()) + + +class TestDistributeVertically: + def test_equal_gaps_between_the_outer_two(self, scene): + boxes = ((0, 0, 100, 40), (10, 70, 100, 20), (20, 400, 100, 60)) + nodes = [] + for x, y, w, h in boxes: + node = DiagramNode(x=x, y=y, w=w, h=h, text="n") + scene.addItem(node) + node.setSelected(True) + nodes.append(node) + + scene.distribute_v() + + tops = [node.pos().y() for node in nodes] + assert tops[0] == 0 and tops[2] == 400 + assert tops[1] - 40 == pytest.approx(400 - (tops[1] + 20)) + assert [node.pos().x() for node in nodes] == [0, 10, 20] From 4fcd3cce2332641eb2c8c8cf18826cfd675adf42 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:41:04 +0800 Subject: [PATCH 143/166] Test the property panel's node edits and colour button --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- .../test_utils/test_diagram_property_panel.py | 76 +++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 549b7868..ebb599f7 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2655 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2660 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 1486b937..67144d8c 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -769,3 +769,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_diagram_scene_actions.py` (new), `architecture_explore.md` (test count) - **Checks**: the eleven tests pass as the code stands (no behaviour needed changing); the diagram tests now cover 91 % of `diagram_scene.py`. `test/test_utils` collects 2655 tests in 147 files. - **Open items**: none. + +## U-20260925-92 · 2026-09-25 · The property panel's node edits and colour button are tested · #test #diagram + +- **What**: `diagram_property_panel.py` had 68.9 % coverage on `main`. A connection's and an image's edits were tested; a node's text, shape, fill and border edits were not, nor the colour button's dialog, nor an edit arriving with nothing selected. +- **Change**: in `test_diagram_property_panel.py`: a node's text, shape and both colours follow the panel, four undo steps; leaving the text box unchanged records nothing (its `editingFinished` comes with every focus change, and a step would mark a saved diagram as changed); an edit with nothing selected changes nothing; the colour button shows and passes on a picked colour, and a cancelled dialog changes nothing. +- **Files**: `test/test_utils/test_diagram_property_panel.py`, `architecture_explore.md` (test count) +- **Checks**: the file's ten tests pass as the code stands. `test/test_utils` collects 2660 tests. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0f28d0d7..53b694f4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-92 | 2026-09-25 | The property panel's node edits and colour button are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-91 | 2026-09-25 | The diagram canvas's clicks and keys are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-90 | 2026-09-25 | The diagram canvas's zoom and grid are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-89 | 2026-09-25 | A node and an image resize through one function | #refactor #diagram | [2026-09-c](2026-09-c.md) | @@ -494,4 +495,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 91 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 92 | diff --git a/test/test_utils/test_diagram_property_panel.py b/test/test_utils/test_diagram_property_panel.py index 16e885b8..9ca40187 100644 --- a/test/test_utils/test_diagram_property_panel.py +++ b/test/test_utils/test_diagram_property_panel.py @@ -105,3 +105,79 @@ def test_a_width_edit_keeps_its_height_and_the_caption_follows(self, image): assert (picture.img_w, picture.img_h) == (200, 80) assert picture.text() == "logo" + + +@pytest.fixture +def node(app): + scene = DiagramScene() + box = DiagramNode(x=0, y=0, w=140, h=60, text="A") + scene.addItem(box) + panel = DiagramPropertyPanel(scene) + box.setSelected(True) + yield scene, box, panel + panel.deleteLater() + scene.deleteLater() + + +class TestANode: + def test_its_text_shape_and_colours_follow_the_panel(self, node): + scene, box, panel = node + + panel._node_text.setText("Start") + panel._node_text.editingFinished.emit() + panel._node_shape.setCurrentIndex(3) + panel._node_fill.color_changed.emit(QColor("#00ff00")) + panel._node_border.color_changed.emit(QColor("#0000ff")) + + saved = box.to_dict(0) + assert (saved["text"], saved["shape"]) == ("Start", "DIAMOND") + assert (saved["fill_color"], saved["border_color"]) == ("#00ff00", "#0000ff") + assert scene.undo_stack.count() == 4 + + def test_leaving_the_text_box_unchanged_records_nothing(self, node): + # editingFinished comes with every focus change: a step for it would + # mark a saved diagram as changed + scene, _box, panel = node + + panel._node_text.editingFinished.emit() + + assert not scene.undo_stack.canUndo() + + def test_an_edit_with_nothing_selected_changes_nothing(self, node): + scene, box, panel = node + box.setSelected(False) + + panel._on_node_fill(QColor("#00ff00")) + panel._on_conn_label() + panel._on_img_caption() + + assert box.to_dict(0)["fill_color"] != "#00ff00" + assert not scene.undo_stack.canUndo() + + +class TestTheColourButton: + def test_a_colour_picked_is_shown_and_passed_on(self, node, monkeypatch): + from pybreeze.pybreeze_ui.diagram_editor import diagram_property_panel + + _scene, box, panel = node + monkeypatch.setattr(diagram_property_panel.QColorDialog, "getColor", + staticmethod(lambda *args: QColor("#123456"))) + + panel._node_fill._pick() + + assert panel._node_fill.color() == QColor("#123456") + assert panel._node_fill.text() == "#123456" + assert box.to_dict(0)["fill_color"] == "#123456" + + def test_a_cancelled_dialog_changes_nothing(self, node, monkeypatch): + from pybreeze.pybreeze_ui.diagram_editor import diagram_property_panel + + scene, _box, panel = node + before = panel._node_fill.color() + monkeypatch.setattr(diagram_property_panel.QColorDialog, "getColor", + staticmethod(lambda *args: QColor())) # what Cancel returns + + panel._node_fill._pick() + + assert panel._node_fill.color() == before + assert not scene.undo_stack.canUndo() From 0bcadce775c37d0f6a00743f75f0864f84199c92 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:43:59 +0800 Subject: [PATCH 144/166] Test the prthinker menu beyond reviewing a file --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++ docs/updates/README.md | 3 +- test/test_utils/test_prthinker_menu_review.py | 82 +++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index ebb599f7..658450d9 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2660 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2665 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 67144d8c..1a887bac 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -777,3 +777,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_diagram_property_panel.py`, `architecture_explore.md` (test count) - **Checks**: the file's ten tests pass as the code stands. `test/test_utils` collects 2660 tests. - **Open items**: none. + +## U-20260925-93 · 2026-09-25 · The prthinker menu is tested beyond reviewing a file · #test #prthinker + +- **What**: `build_prthinker_menu.py` had 42.6 % coverage on `main` (43 % locally): only Review the current file was tested. Review a Pull Request, Settings and the menu itself were not. +- **Change**: in `test_prthinker_menu_review.py`: the pull request number asked for (from 1 to 1,000,000, starting at 1) is the one reviewed, cancelling the question reviews nothing, and without a repository set the user is told so; the menu's entries read Review the current file, Review a Pull Request, Settings and Help, and Settings, the documentation and GitHub open what they should; Settings opens as a dialog of the main window, deleted on close. +- **Files**: `test/test_utils/test_prthinker_menu_review.py`, `architecture_explore.md` (test count) +- **Checks**: the file's eight tests pass as the code stands. `test/test_utils` collects 2665 tests. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 53b694f4..f0b2826e 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-93 | 2026-09-25 | The prthinker menu is tested beyond reviewing a file | #test #prthinker | [2026-09-c](2026-09-c.md) | | U-20260925-92 | 2026-09-25 | The property panel's node edits and colour button are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-91 | 2026-09-25 | The diagram canvas's clicks and keys are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-90 | 2026-09-25 | The diagram canvas's zoom and grid are tested | #test #diagram | [2026-09-c](2026-09-c.md) | @@ -495,4 +496,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 92 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 93 | diff --git a/test/test_utils/test_prthinker_menu_review.py b/test/test_utils/test_prthinker_menu_review.py index a5ea9021..b74fd0d5 100644 --- a/test/test_utils/test_prthinker_menu_review.py +++ b/test/test_utils/test_prthinker_menu_review.py @@ -57,3 +57,85 @@ def test_a_tab_that_is_not_an_editor_says_what_is_needed(window, monkeypatch): menu._review_current_file(window) assert window.events == [("told", "prthinker_need_saved_file_message")] + + +class TestReviewAPullRequest: + @staticmethod + def _answer(monkeypatch, number: int, chosen: bool) -> list: + asked: list = [] + + def get_int(*args): + asked.append(args[3:]) # the value it starts at, the lowest and the highest + return number, chosen + + monkeypatch.setattr(menu.QInputDialog, "getInt", staticmethod(get_int)) + return asked + + def test_the_number_asked_for_is_reviewed(self, window, monkeypatch): + asked = self._answer(monkeypatch, 42, True) + monkeypatch.setattr( + menu, "review_pull_request", lambda _window, number: window.events.append(number) or True) + + menu._review_pull_request(window) + + assert window.events == [42] + assert asked == [(1, 1, 1000000)] + + def test_cancelling_the_question_reviews_nothing(self, window, monkeypatch): + self._answer(monkeypatch, 1, False) + monkeypatch.setattr( + menu, "review_pull_request", lambda _window, number: window.events.append(number) or True) + + menu._review_pull_request(window) + + assert window.events == [] + + def test_without_a_repository_set_it_says_so(self, window, monkeypatch): + self._answer(monkeypatch, 7, True) + monkeypatch.setattr(menu, "review_pull_request", lambda _window, _number: False) + + menu._review_pull_request(window) + + assert window.events == [("told", "prthinker_need_repository_message")] + + +class TestTheMenu: + def test_its_entries_and_what_they_open(self, window, monkeypatch): + from PySide6.QtWidgets import QMenu + + from pybreeze.extend_multi_language.update_language_dict import update_language_dict + + update_language_dict() + window.automation_menu = QMenu() + opened: list = [] + monkeypatch.setattr(menu, "open_web_browser", lambda _window, url, _title: opened.append(url)) + monkeypatch.setattr(menu, "_open_setting", lambda _window: opened.append("settings")) + + menu.set_prthinker_menu(window) + window.prthinker_setting_action.trigger() + window.prthinker_doc_action.trigger() + window.prthinker_github_action.trigger() + + labels = [action.text() for action in window.prthinker_menu.actions()] + assert labels == ["Review the current file", "Review a Pull Request", "Settings", "Help"] + assert opened == ["settings", menu.DOCUMENT_URL, menu.GITHUB_URL] + window.automation_menu.deleteLater() + + def test_the_settings_open_as_a_dialog_of_the_window(self, window, monkeypatch): + shown: list = [] + + class Dialog: + def __init__(self, parent): + shown.append(parent) + + def setAttribute(self, *_args): + shown.append("deleted on close") + + def exec(self): + shown.append("shown") + + monkeypatch.setattr(menu, "PRThinkerSettingDialog", Dialog) + + menu._open_setting(window) + + assert shown == [window, "deleted on close", "shown"] From 531a51b9f821664a5f750bc863db7d6cfed594fb Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:45:32 +0800 Subject: [PATCH 145/166] Open every Tools tab entry in a test --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++++++ docs/updates/README.md | 3 ++- test/test_utils/test_tools_menu_docks.py | 29 ++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 658450d9..e03e5bdf 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2665 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2666 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 1a887bac..a3c09929 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -785,3 +785,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_prthinker_menu_review.py`, `architecture_explore.md` (test count) - **Checks**: the file's eight tests pass as the code stands. `test/test_utils` collects 2665 tests. - **Open items**: none. + +## U-20260925-94 · 2026-09-25 · Every Tools tab entry is opened in a test · #test #menu + +- **What**: `test_tools_menu_docks.py` opened every tool as a dock, and checked that each tool has a tab entry, but no test triggered the tab entries: a factory that failed, or a label key with no text, would have shown only in the running IDE (the Tools menu is built at start-up, which the unit tests reach only in child processes that coverage does not follow). +- **Change**: `test_every_tools_tab_entry_opens_its_widget_under_its_label` builds the Tools menu on a stand-in main window with the home folder in a scratch directory, and triggers all twenty tab entries: each sits in its submenu, reads its dictionary text, and opens its widget in a tab under its label. +- **Files**: `test/test_utils/test_tools_menu_docks.py`, `architecture_explore.md` (test count) +- **Checks**: the file's seventeen tests pass as the code stands. `test/test_utils` collects 2666 tests. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index f0b2826e..b86d44b8 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-94 | 2026-09-25 | Every Tools tab entry is opened in a test | #test #menu | [2026-09-c](2026-09-c.md) | | U-20260925-93 | 2026-09-25 | The prthinker menu is tested beyond reviewing a file | #test #prthinker | [2026-09-c](2026-09-c.md) | | U-20260925-92 | 2026-09-25 | The property panel's node edits and colour button are tested | #test #diagram | [2026-09-c](2026-09-c.md) | | U-20260925-91 | 2026-09-25 | The diagram canvas's clicks and keys are tested | #test #diagram | [2026-09-c](2026-09-c.md) | @@ -496,4 +497,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 93 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 94 | diff --git a/test/test_utils/test_tools_menu_docks.py b/test/test_utils/test_tools_menu_docks.py index cfbb3a24..ff05194f 100644 --- a/test/test_utils/test_tools_menu_docks.py +++ b/test/test_utils/test_tools_menu_docks.py @@ -84,3 +84,32 @@ def test_every_tool_widget_has_a_tab_a_dock_and_a_dock_title(): assert {entry[0] for entry in tools_menu._TAB_ACTIONS} == factories assert {entry[0] for entry in tools_menu._DOCK_ACTIONS} == factories assert set(tools_menu._DOCK_TITLES) == factories + + +def test_every_tools_tab_entry_opens_its_widget_under_its_label(app, tmp_path, monkeypatch): + # The docks are checked above; the tab entries were only built, never opened + from je_editor import language_wrapper + from PySide6.QtWidgets import QTabWidget + + monkeypatch.setenv("USERPROFILE", str(tmp_path)) # the prompt editors and review stats live under ~ + monkeypatch.setenv("HOME", str(tmp_path)) + window = QMainWindow() + window.menu = window.menuBar() + window.tab_widget = QTabWidget() + try: + tools_menu.build_tools_menu(window) + for widget_key, attribute, menu_attribute, action_key, label_key in tools_menu._TAB_ACTIONS: + action = getattr(window, attribute) + assert action in getattr(window, menu_attribute).actions() + assert action.text() == language_wrapper.language_word_dict.get(action_key) + + action.trigger() + + index = window.tab_widget.count() - 1 + assert window.tab_widget.tabText(index) == language_wrapper.language_word_dict.get(label_key), widget_key + assert window.tab_widget.widget(index) is not None + assert window.tab_widget.count() == len(tools_menu._TAB_ACTIONS) + finally: + for index in range(window.tab_widget.count()): + window.tab_widget.widget(index).close() + window.deleteLater() From 03b7a232d2fb3944ccf45cbedfacc4fd1867d1dd Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:48:32 +0800 Subject: [PATCH 146/166] Complete architecture.md's custom-tab extension point --- architecture.md | 4 +++- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/architecture.md b/architecture.md index 0f6237e3..4c1e5f68 100644 --- a/architecture.md +++ b/architecture.md @@ -106,7 +106,9 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() ## 5. Extension points - **Custom tabs**: add entries to `EDITOR_EXTEND_TAB` (`pybreeze_ui/editor_main/main_ui.py`) before - `start_editor()`. + `start_editor()`, or in a file plugin's `register()`, which runs before the tabs are added. A widget + with a `may_close()` is asked before its tab, its dock or the IDE closes (`pybreeze_ui/closing.py`); + one whose constructor raises costs only its own tab. - **File plugins**: `jeditor_plugins/` in the working directory, loaded by JEditor (`je_editor/plugins/plugin_loader.py`). `PLUGIN_RUN_CONFIG` entries appear in the Run with… and Plugins menus and execute via `FileRunnerProcess`. The plugin browser tab reuses JEditor's diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index a3c09929..05056374 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -793,3 +793,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_tools_menu_docks.py`, `architecture_explore.md` (test count) - **Checks**: the file's seventeen tests pass as the code stands. `test/test_utils` collects 2666 tests. - **Open items**: none. + +## U-20260925-95 · 2026-09-25 · architecture.md's custom-tab extension point is complete · #docs + +- **What**: `architecture.md` §5 said custom tabs are added to `EDITOR_EXTEND_TAB` before `start_editor()`, which the Sphinx How to Extend the UI page (U-20260925-73) says more fully: a file plugin can register one in `register()`, a widget with `may_close()` is asked before it closes, and a constructor that raises costs only its own tab. +- **Change**: §5's Custom tabs entry says all three, pointing at `pybreeze_ui/closing.py`. +- **Files**: `architecture.md` +- **Checks**: checked against `main_ui.py` (`_add_extend_tabs`, `close_tab`, `_tool_tabs_may_close`) and `closing.py`. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b86d44b8..e7a764c9 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-95 | 2026-09-25 | architecture.md's custom-tab extension point is complete | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-94 | 2026-09-25 | Every Tools tab entry is opened in a test | #test #menu | [2026-09-c](2026-09-c.md) | | U-20260925-93 | 2026-09-25 | The prthinker menu is tested beyond reviewing a file | #test #prthinker | [2026-09-c](2026-09-c.md) | | U-20260925-92 | 2026-09-25 | The property panel's node edits and colour button are tested | #test #diagram | [2026-09-c](2026-09-c.md) | @@ -497,4 +498,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 94 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 95 | From fae296ad279f9514783f5977edda967e6052f181 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:52:42 +0800 Subject: [PATCH 147/166] Test the SFTP tree's download and upload dialogs --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- test/test_utils/test_sftp_tree_actions.py | 85 +++++++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index e03e5bdf..cae84b40 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2666 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2673 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 05056374..a2fda8ea 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -801,3 +801,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `architecture.md` - **Checks**: checked against `main_ui.py` (`_add_extend_tabs`, `close_tab`, `_tool_tabs_may_close`) and `closing.py`. - **Open items**: none. + +## U-20260925-96 · 2026-09-25 · The SFTP tree's download and upload dialogs are tested · #test #ssh + +- **What**: the SFTP tree's transfers were tested from `_start_transfer` on (replacing, cancelling, temporary files, freeing), but not the actions in front of them: Download, Upload and New Folder with no place chosen had no test (86 % coverage of `ssh_file_viewer_widget.py` locally). +- **Change**: in `test_sftp_tree_actions.py`: a file downloads to the path the save dialog gives, which offered the file's own name; a cancelled save or a folder downloads nothing (a folder is told so); an upload goes into the folder chosen, or into the folder of the file chosen, and refreshes that folder once it is done; a cancelled choice uploads nothing; New Folder with nothing chosen says so and creates nothing. +- **Files**: `test/test_utils/test_sftp_tree_actions.py`, `architecture_explore.md` (test count) +- **Checks**: the seven tests pass as the code stands. `test/test_utils` collects 2673 tests. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index e7a764c9..b18592e2 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-96 | 2026-09-25 | The SFTP tree's download and upload dialogs are tested | #test #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-95 | 2026-09-25 | architecture.md's custom-tab extension point is complete | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-94 | 2026-09-25 | Every Tools tab entry is opened in a test | #test #menu | [2026-09-c](2026-09-c.md) | | U-20260925-93 | 2026-09-25 | The prthinker menu is tested beyond reviewing a file | #test #prthinker | [2026-09-c](2026-09-c.md) | @@ -498,4 +499,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 95 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 96 | diff --git a/test/test_utils/test_sftp_tree_actions.py b/test/test_utils/test_sftp_tree_actions.py index bfbe0257..411ec4c2 100644 --- a/test/test_utils/test_sftp_tree_actions.py +++ b/test/test_utils/test_sftp_tree_actions.py @@ -412,3 +412,88 @@ def dropped(_item): self._shortcut(widget, "Del").activated.emit() assert reported and "Socket is closed" in reported[0] + + +class TestDownloadAndUpload: + """The dialogs in front of a transfer, and what the transfer is then asked to do.""" + + @staticmethod + def _transfers(widget, monkeypatch) -> list[dict]: + started: list[dict] = [] + monkeypatch.setattr(widget, "_start_transfer", lambda **kwargs: started.append(kwargs) or True) + return started + + @staticmethod + def _informed(monkeypatch) -> list: + told: list = [] + monkeypatch.setattr(tree_mod.QMessageBox, "information", lambda *args: told.append(args[2])) + return told + + def test_a_file_downloads_to_where_it_is_saved(self, tree, monkeypatch, tmp_path): + widget, _client, root, _warnings = tree + started = self._transfers(widget, monkeypatch) + offered: list = [] + target = str(tmp_path / "copy.txt") + monkeypatch.setattr(tree_mod.QFileDialog, "getSaveFileName", + staticmethod(lambda _parent, _title, name: offered.append(name) or (target, ""))) + + widget.action_download(_child(root, "notes.txt")) + + assert offered == ["notes.txt"] + assert [(s["downloading"], s["remote_path"], s["local_path"]) for s in started] == [ + (True, "/notes.txt", target)] + + def test_a_cancelled_save_downloads_nothing(self, tree, monkeypatch): + widget, _client, root, _warnings = tree + started = self._transfers(widget, monkeypatch) + monkeypatch.setattr(tree_mod.QFileDialog, "getSaveFileName", staticmethod(lambda *args: ("", ""))) + + widget.action_download(_child(root, "notes.txt")) + + assert started == [] + + def test_a_folder_is_not_downloaded(self, tree, monkeypatch): + widget, _client, root, _warnings = tree + started = self._transfers(widget, monkeypatch) + told = self._informed(monkeypatch) + + widget.action_download(_child(root, "src")) + + assert started == [] + assert len(told) == 1 + + @pytest.mark.parametrize(("name", "remote"), [("src", "/src/upload.bin"), ("notes.txt", "/upload.bin")]) + def test_an_upload_goes_into_the_folder_chosen(self, tree, monkeypatch, tmp_path, name, remote): + # A file chosen in the tree stands for the folder it is in + widget, _client, root, _warnings = tree + started = self._transfers(widget, monkeypatch) + local = tmp_path / "upload.bin" + local.write_bytes(b"x") + monkeypatch.setattr(tree_mod.QFileDialog, "getOpenFileName", staticmethod(lambda *args: (str(local), ""))) + refreshed: list = [] + monkeypatch.setattr(widget, "action_refresh", lambda item: refreshed.append(item.text(3))) + + widget.action_upload(_child(root, name)) + started[0]["after"]() + + assert [(s["downloading"], s["remote_path"], s["local_path"]) for s in started] == [ + (False, remote, str(local))] + assert refreshed == [remote.rsplit("/", 1)[0] or "/"] + + def test_a_cancelled_choice_uploads_nothing(self, tree, monkeypatch): + widget, _client, root, _warnings = tree + started = self._transfers(widget, monkeypatch) + monkeypatch.setattr(tree_mod.QFileDialog, "getOpenFileName", staticmethod(lambda *args: ("", ""))) + + widget.action_upload(_child(root, "src")) + + assert started == [] + + def test_a_folder_needs_a_place_to_be_created_in(self, tree, monkeypatch): + widget, client, _root, _warnings = tree + told = self._informed(monkeypatch) + + widget.action_create_folder(None) + + assert len(told) == 1 + assert client.made == [] From dc0449b0fdc810e7b68b37093faa925ec45be32e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:56:12 +0800 Subject: [PATCH 148/166] Test the SSH terminal's key authentication and input checks --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- test/test_utils/test_ssh_reentrancy.py | 75 ++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index cae84b40..00058a36 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2673 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2678 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index a2fda8ea..dbf9a007 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -809,3 +809,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_sftp_tree_actions.py`, `architecture_explore.md` (test count) - **Checks**: the seven tests pass as the code stands. `test/test_utils` collects 2673 tests. - **Open items**: none. + +## U-20260925-97 · 2026-09-25 · The SSH terminal's key authentication and input checks are tested · #test #ssh + +- **What**: that every connect refuses SHA-1 was tested for the terminal's password login and the file tree, not for the terminal's key login (`_connect_with_key`), the path CLAUDE.md's SSH rule covers just the same. Also untested: the terminal's checks before it connects, and what an encrypted key with no passphrase says. +- **Change**: in `test_ssh_reentrancy.py`: key authentication passes `SHA1_ALGORITHMS` and the loaded key, and no password; a key needing a passphrase, with none given, connects nothing and the terminal shows the passphrase message; an empty host or user, or a key file that is not there, is refused with a warning before any connect starts. +- **Files**: `test/test_utils/test_ssh_reentrancy.py`, `architecture_explore.md` (test count) +- **Checks**: the file's 21 tests pass as the code stands. `test/test_utils` collects 2678 tests. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index b18592e2..c1ee2803 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-97 | 2026-09-25 | The SSH terminal's key authentication and input checks are tested | #test #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-96 | 2026-09-25 | The SFTP tree's download and upload dialogs are tested | #test #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-95 | 2026-09-25 | architecture.md's custom-tab extension point is complete | #docs | [2026-09-c](2026-09-c.md) | | U-20260925-94 | 2026-09-25 | Every Tools tab entry is opened in a test | #test #menu | [2026-09-c](2026-09-c.md) | @@ -499,4 +500,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 96 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 97 | diff --git a/test/test_utils/test_ssh_reentrancy.py b/test/test_utils/test_ssh_reentrancy.py index 94ab57aa..83dc9925 100644 --- a/test/test_utils/test_ssh_reentrancy.py +++ b/test/test_utils/test_ssh_reentrancy.py @@ -406,3 +406,78 @@ def close(self) -> None: sftp_session.SFTPClientWrapper().connect("host", 22, "user", "pw") assert connects[0]["disabled_algorithms"] is sftp_session.SHA1_ALGORITHMS + + +class TestKeyAuthentication: + @staticmethod + def _key_file(tmp_path, password: str | None = None) -> str: + import paramiko + + path = tmp_path / "id_rsa" + paramiko.RSAKey.generate(2048).write_private_key_file(str(path), password=password) + return str(path) + + @staticmethod + def _use_key(widget, key_path: str, passphrase: str = "") -> None: + widget.login_widget.use_key_check.setChecked(True) + widget.login_widget.key_edit.setText(key_path) + widget.login_widget.pass_edit.setText(passphrase) + + def test_it_refuses_sha1_as_the_password_does(self, app, monkeypatch, tmp_path): + import paramiko + + client = FakeClient() + widget = _shell(monkeypatch, client) + widget._start_shell = lambda *args: None + self._use_key(widget, self._key_file(tmp_path)) + + widget.connect_ssh() + _wait_for(lambda: client.connect_options and not widget._connecting.isRunning()) + + assert client.connect_options["disabled_algorithms"] is shell_mod.SHA1_ALGORITHMS + assert isinstance(client.connect_options["pkey"], paramiko.RSAKey) + assert "password" not in client.connect_options + + def test_a_key_needing_a_passphrase_says_so_and_connects_nothing(self, app, monkeypatch, tmp_path): + client = FakeClient() + widget = _shell(monkeypatch, client) + self._use_key(widget, self._key_file(tmp_path, password="secret")) + + widget.connect_ssh() + _wait_for(lambda: not widget._connecting.isRunning()) + _wait_for(lambda: widget.ssh_client is None) + + from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_key_loader import PASSPHRASE_NEEDED + + assert client.connect_options == {} + assert widget.word_dict.get(PASSPHRASE_NEEDED) in widget.terminal.toPlainText() + + +class TestConnectInputs: + @staticmethod + def _warned(monkeypatch) -> list: + warned: list = [] + monkeypatch.setattr(shell_mod.QMessageBox, "warning", lambda *args: warned.append(args[2])) + return warned + + @pytest.mark.parametrize("empty", ["host_edit", "user_edit"]) + def test_host_and_user_are_required(self, app, monkeypatch, empty): + widget = _shell(monkeypatch, FakeClient()) + getattr(widget.login_widget, empty).setText(" ") + warned = self._warned(monkeypatch) + + widget.connect_ssh() + + assert len(warned) == 1 + assert widget._connecting is None + + def test_a_key_file_that_is_not_there_is_refused(self, app, monkeypatch, tmp_path): + widget = _shell(monkeypatch, FakeClient()) + widget.login_widget.use_key_check.setChecked(True) + widget.login_widget.key_edit.setText(str(tmp_path / "missing_key")) + warned = self._warned(monkeypatch) + + widget.connect_ssh() + + assert len(warned) == 1 + assert widget._connecting is None From 1124bc56ad790ea05c6cac1072aa0b48631e833e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 14:59:43 +0800 Subject: [PATCH 149/166] Test installing JupyterLab for the tab --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- test/test_utils/test_jupyter_ready.py | 83 +++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 00058a36..2e7a04e5 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2678 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2682 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index dbf9a007..ed36d1d8 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -817,3 +817,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_ssh_reentrancy.py`, `architecture_explore.md` (test count) - **Checks**: the file's 21 tests pass as the code stands. `test/test_utils` collects 2678 tests. - **Open items**: none. + +## U-20260925-98 · 2026-09-25 · Installing JupyterLab for the tab is tested · #test #jupyter + +- **What**: the JupyterLab tab installs JupyterLab into the interpreter it runs in when that interpreter has none (U-20260925-62 made it the interpreter a run uses); that path had no test (`jupyter_lab_thread.py` 84 %, `jupyter_lab_widget.py` 75 % locally), nor the tab's switch from its status line to the lab. +- **Change**: in `test_jupyter_ready.py`: a missing JupyterLab is installed with `python -m pip install jupyterlab -U` into the interpreter the lab runs in, with the five-minute limit, the tab says it is downloading, and the server then starts on the port found and is announced at `http://localhost:/lab`; a failed install shows pip's reason and starts no server; a tab closed during the install gets no server and no error; once the lab loads, the status line is gone and a late status or error does nothing. +- **Files**: `test/test_utils/test_jupyter_ready.py`, `architecture_explore.md` (test count) +- **Checks**: the four tests pass as the code stands. `test/test_utils` collects 2682 tests. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index c1ee2803..606614fd 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-98 | 2026-09-25 | Installing JupyterLab for the tab is tested | #test #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-97 | 2026-09-25 | The SSH terminal's key authentication and input checks are tested | #test #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-96 | 2026-09-25 | The SFTP tree's download and upload dialogs are tested | #test #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-95 | 2026-09-25 | architecture.md's custom-tab extension point is complete | #docs | [2026-09-c](2026-09-c.md) | @@ -500,4 +501,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 97 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 98 | diff --git a/test/test_utils/test_jupyter_ready.py b/test/test_utils/test_jupyter_ready.py index 6ad9ccc7..93cafeec 100644 --- a/test/test_utils/test_jupyter_ready.py +++ b/test/test_utils/test_jupyter_ready.py @@ -292,3 +292,86 @@ def test_the_server_is_told_not_to_move_to_another_port(monkeypatch): assert "--ServerApp.port_retries=0" in started[0] assert "--ServerApp.port=8888" in started[0] + + +class TestInstallingJupyterLab: + """When the interpreter has no JupyterLab, it is installed there first.""" + + @staticmethod + def _launch(monkeypatch, pip_result, during_pip=None): + from types import SimpleNamespace + + from pybreeze.pybreeze_ui.jupyter_lab_gui import jupyter_lab_thread as mod + + seen: dict = {"pip": [], "started": [], "status": [], "errors": [], "ready": []} + thread = mod.JupyterLauncherThread(python_exe="C:/envs/lab/python.exe") + + def run(args, **options): + seen["pip"].append((args, options.get("timeout"))) + if during_pip is not None: + during_pip(thread) + return SimpleNamespace(returncode=pip_result[0], stderr=pip_result[1]) + + monkeypatch.setattr(mod, "is_jupyter_installed", lambda exe: False) + monkeypatch.setattr(mod.subprocess, "run", run) + monkeypatch.setattr(mod, "find_free_port", lambda: 58888) + monkeypatch.setattr(thread, "_start_server", lambda exe, port: seen["started"].append((exe, port)) or object()) + monkeypatch.setattr(thread, "_wait_until_ready", lambda port: None) + thread.status_update.connect(seen["status"].append) + thread.error_occurred.connect(seen["errors"].append) + thread.server_ready.connect(seen["ready"].append) + thread.run() + return seen + + def test_it_is_installed_into_the_interpreter_the_lab_runs_in(self, qt_app, monkeypatch): + from je_editor import language_wrapper + + seen = self._launch(monkeypatch, (0, "")) + + assert seen["pip"] == [(["C:/envs/lab/python.exe", "-m", "pip", "install", "jupyterlab", "-U"], 300)] + assert seen["status"][0] == language_wrapper.language_word_dict.get("jupyterlab_downloading") + assert seen["started"] == [("C:/envs/lab/python.exe", 58888)] + assert seen["ready"] == ["http://localhost:58888/lab"] + assert seen["errors"] == [] + + def test_a_failed_install_says_why_and_starts_no_server(self, qt_app, monkeypatch): + monkeypatch.setattr("pybreeze.pybreeze_ui.jupyter_lab_gui.jupyter_lab_thread.pybreeze_logger.error", + lambda *args: None) + + seen = self._launch(monkeypatch, (1, "ERROR: No matching distribution found for jupyterlab")) + + assert seen["errors"] == ["ERROR: No matching distribution found for jupyterlab"] + assert seen["started"] == [] + assert seen["ready"] == [] + + def test_a_tab_closed_during_the_install_gets_no_server(self, qt_app, monkeypatch): + seen = self._launch(monkeypatch, (0, ""), during_pip=lambda thread: thread.stop()) + + assert seen["started"] == [] + assert seen["ready"] == [] + assert seen["errors"] == [] + + +class TestTheTab: + @staticmethod + def _tab(monkeypatch): + from pybreeze.extend_multi_language.update_language_dict import update_language_dict + from pybreeze.pybreeze_ui.jupyter_lab_gui import jupyter_lab_widget + + update_language_dict() + monkeypatch.setattr(jupyter_lab_widget.JupyterLauncherThread, "start", lambda self: None) + return jupyter_lab_widget.JupyterLabWidget() + + def test_the_lab_replaces_the_status_and_a_late_status_is_ignored(self, qt_app, monkeypatch): + tab = self._tab(monkeypatch) + tab.update_status("Loading...") + assert tab.status_label.text() == "Loading..." + + tab.load_lab("http://localhost:58888/lab") + tab.update_status("Loading... (3s / 60s)") # queued before the lab was ready + tab.show_error("too late to matter") + + assert tab.status_label is None + assert tab.browser.url().toString() == "http://localhost:58888/lab" + tab.close() + tab.deleteLater() From 1dff6f299bf6b29e7d37ef5c370e1c6cd1d5d5d9 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 15:27:09 +0800 Subject: [PATCH 150/166] Keep the JupyterLab tab test from crashing the test run on exit --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- test/test_utils/test_jupyter_ready.py | 14 +++++++++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index ed36d1d8..d93641b2 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -825,3 +825,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_jupyter_ready.py`, `architecture_explore.md` (test count) - **Checks**: the four tests pass as the code stands. `test/test_utils` collects 2682 tests. - **Open items**: none. + +## U-20260925-99 · 2026-09-25 · The JupyterLab tab test no longer crashes the test run on exit · #test #fix #jupyter + +- **What**: after U-20260925-98 the whole `test/test_utils` run reported 2667 passed and 15 skipped but exited with 139, which fails the CI job. `test_jupyter_ready.py` alone reproduced it every time, down to `TestTheTab`: its `load_lab()` had Chromium load a page, and the view (closed and given to `deleteLater()`, which no event loop carried out) was still alive when the interpreter ended. +- **Change**: the test records the URL the tab gives its web view instead of loading it, checks that the view is shown, and carries out the tab's delete (`sendPostedEvents(tab, DeferredDelete)`) before it ends. The widget is unchanged: a script that loads the lab in a tab, closes it the way the IDE does and quits the event loop exits with 0 (twice), while the test's old steps exit with 139 (twice). +- **Files**: `test/test_utils/test_jupyter_ready.py` +- **Checks**: `test_jupyter_ready.py` with `test_jupyter_lifecycle.py` exits with 0 three times out of three (27 passed each). +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 606614fd..6994dd4c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-99 | 2026-09-25 | The JupyterLab tab test no longer crashes the test run on exit | #test #fix #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-98 | 2026-09-25 | Installing JupyterLab for the tab is tested | #test #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-97 | 2026-09-25 | The SSH terminal's key authentication and input checks are tested | #test #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-96 | 2026-09-25 | The SFTP tree's download and upload dialogs are tested | #test #ssh | [2026-09-c](2026-09-c.md) | @@ -501,4 +502,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 98 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 99 | diff --git a/test/test_utils/test_jupyter_ready.py b/test/test_utils/test_jupyter_ready.py index 93cafeec..6842a8c4 100644 --- a/test/test_utils/test_jupyter_ready.py +++ b/test/test_utils/test_jupyter_ready.py @@ -363,7 +363,14 @@ def _tab(monkeypatch): return jupyter_lab_widget.JupyterLabWidget() def test_the_lab_replaces_the_status_and_a_late_status_is_ignored(self, qt_app, monkeypatch): + from PySide6.QtCore import QCoreApplication, QEvent + tab = self._tab(monkeypatch) + # Recorded, not loaded: a page Chromium had loaded, in a view still alive + # when the test process ended, crashed the interpreter on its way out + # (exit 139 after every test had passed) + loaded: list[str] = [] + tab.browser.setUrl = lambda url: loaded.append(url.toString()) tab.update_status("Loading...") assert tab.status_label.text() == "Loading..." @@ -372,6 +379,7 @@ def test_the_lab_replaces_the_status_and_a_late_status_is_ignored(self, qt_app, tab.show_error("too late to matter") assert tab.status_label is None - assert tab.browser.url().toString() == "http://localhost:58888/lab" - tab.close() - tab.deleteLater() + assert loaded == ["http://localhost:58888/lab"] + assert tab.browser.isVisibleTo(tab) + tab.close() # deleted on close; the delete is carried out here, not at exit + QCoreApplication.sendPostedEvents(tab, QEvent.Type.DeferredDelete) From 706f21da87a8d577d921c4928d541560d2da25ba Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 15:41:49 +0800 Subject: [PATCH 151/166] Count the tests that start the main window in a child in coverage --- .coveragerc | 12 +++++++++--- CLAUDE.md | 2 +- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index e2f9790c..27664856 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,12 +1,18 @@ # Coverage is measured on Windows and consumed by the SonarQube scanner running # on Linux, so the report must not carry machine-specific paths. relative_files -# makes coverage emit repo-root-relative names (pybreeze/utils/... rather than an -# absolute plus a package-relative filename), which is what the scanner -# resolves against. +# makes the report's the relative "pybreeze" rather than an absolute +# path, and the scanner resolves the package-relative filenames against it. +# +# patch = subprocess starts coverage in every Python child a test starts (it +# needs coverage 7.10+; pytest-cov 7 no longer measures children itself), so +# the tests that build the real main window in a child interpreter +# (test/test_utils/started_window.py) count. Each child writes its own data +# file, which pytest-cov combines before reporting. [run] relative_files = True source = pybreeze branch = True +patch = subprocess [report] exclude_also = diff --git a/CLAUDE.md b/CLAUDE.md index 53f00ec4..e496b233 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ pybreeze/ - `unit-tests` job: GitHub Actions on Windows, Python 3.10–3.14 — install deps → pytest `test/test_utils/` → `start_automation_test` → `extend_automation_test` - `sonarcloud` job: CI-based SonarQube Cloud analysis (`sonar-project.properties`), `needs: unit-tests` so it can consume the `coverage-xml` artifact that leg uploads. Automatic Analysis is off and must stay off — the two modes are mutually exclusive and the scanner refuses to run alongside it - SonarCloud's plan for this organization exposes results for `main` and for pull requests only. An analysis pushed for another branch succeeds but its results read back 403, so `dev.yml` scans on pull requests only; `stable.yml` also scans pushes to `main`. Do not "fix" this by scanning every `dev` push — the numbers are not readable -- Coverage comes from the 3.12 matrix leg (`pytest --cov`), configured by `.coveragerc`. `relative_files = True` is required: the report is produced on Windows and consumed by a Linux scanner, so it must not carry machine-specific paths. coverage traces only the threads Python starts, so `test/test_utils/conftest.py` gives every `QThread` subclass a `run` that installs its tracer on Qt's thread; without it no `QThread.run` counts as covered +- Coverage comes from the 3.12 matrix leg (`pytest --cov`), configured by `.coveragerc`. `relative_files = True` is required: the report is produced on Windows and consumed by a Linux scanner, so it must not carry machine-specific paths. `patch = subprocess` is required too: pytest-cov 7 no longer measures child processes, and without it nothing the tests run in a child interpreter (the real main window, `started_window.py`) counts. coverage traces only the threads Python starts, so `test/test_utils/conftest.py` gives every `QThread` subclass a `run` that installs its tracer on Qt's thread; without it no `QThread.run` counts as covered ## Development diff --git a/architecture_explore.md b/architecture_explore.md index 2e7a04e5..fd6d1f65 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -465,7 +465,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 - **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2682 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 -- **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體語句 93%、連分支 91%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`extend_ai_gui` 95%、`extend/` 94%、`connect_gui` 94%、`diagram_editor` 90%、`menu` 85%、`jupyter_lab_gui` 80%;最低的是 `editor_main` 72%——主視窗多半在子行程裡的啟動測試跑,那部分不算進覆蓋率)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 +- **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 - **靜態分析** SonarCloud(`sonar-project.properties`,CI-based analysis;Automatic Analysis 已關閉且必須維持關閉,兩種模式互斥)+ Codacy(`.codacy.yml`)+ Bandit(`pyproject.toml` 中排除 test、skip B101/B404) - **SonarCloud 方案限制** 該組織的方案只開放 `main` 與 PR 的分析結果。非 main 分支的分析送得出去、CE 任務也會成功,但結果讀回來是 403(組織內每個專案都只有 `main` 一條分支)。因此 `dev.yml` 只在 PR 時掃描,`stable.yml` 另外掃 push to `main` diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index d93641b2..316ff6bc 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -833,3 +833,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_jupyter_ready.py` - **Checks**: `test_jupyter_ready.py` with `test_jupyter_lifecycle.py` exits with 0 three times out of three (27 passed each). - **Open items**: none. + +## U-20260925-100 · 2026-09-25 · Coverage counts the tests that start the real main window in a child · #ci #test #coverage + +- **What**: SonarCloud put `main_ui.py` at 31.5 % and `build_menubar.py` at 51.4 %, though tests build the whole main window, menus included: they do it in a child interpreter (`test/test_utils/started_window.py`), and CI installs pytest-cov unpinned, whose 7.0 dropped the measurement of child processes. +- **Change**: `.coveragerc` sets `patch = subprocess` (coverage 7.10+, which pytest-cov 7 requires): every Python child a test starts measures itself into a data file of its own, and pytest-cov combines them before the XML report. The comment on `relative_files` is corrected too: the report carries the relative `pybreeze` and package-relative file names, not repo-root-relative ones. `CLAUDE.md` (Branching & CI) and `architecture_explore.md` §18 say why both settings are required. +- **Files**: `.coveragerc`, `CLAUDE.md`, `architecture_explore.md` +- **Checks**: the start-theme and Stop All tests with and without the setting: `main_ui.py` 0 % → 69 %, `build_menubar.py` 0 % → 100 %; without `COVERAGE_FILE` set, the children (working directory a temporary folder) still write where the parent combines. The XML report has the same file names and `` as before. The full run with it (Python 3.11, coverage 7.14.3, pytest-cov 7.1.0): 2667 passed, 15 skipped, exit 0, in 13 minutes; statements 96.4 %, with branches 94.9 %, `editor_main` 90 %, `menu` 97 %; `architecture_explore.md` quoted 93 %, 91 %, 72 % and 85 %, measured earlier without the children, and now quotes these. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 6994dd4c..ffef5c8b 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-100 | 2026-09-25 | Coverage counts the tests that start the real main window in a child | #ci #test #coverage | [2026-09-c](2026-09-c.md) | | U-20260925-99 | 2026-09-25 | The JupyterLab tab test no longer crashes the test run on exit | #test #fix #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-98 | 2026-09-25 | Installing JupyterLab for the tab is tested | #test #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-97 | 2026-09-25 | The SSH terminal's key authentication and input checks are tested | #test #ssh | [2026-09-c](2026-09-c.md) | @@ -502,4 +503,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 99 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 100 | From 11744492555e5627aaaec715b2fc0fc8801a14b5 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 15:49:54 +0800 Subject: [PATCH 152/166] Import AutoControl when used so Qt can make the IDE per-monitor DPI aware --- CLAUDE.md | 1 + architecture_explore.md | 6 +-- docs/updates/2026-09-c.md | 9 ++++ docs/updates/README.md | 3 +- .../api_testka_menu/build_api_testka_menu.py | 2 +- .../build_autocontrol_menu.py | 32 +++++++++++--- .../automation_menu_factory.py | 15 ++++--- .../build_load_density_menu.py | 2 +- test/test_utils/test_autocontrol_record.py | 44 ++++++++++++++++++- .../test_automation_menu_factory.py | 2 +- test/test_utils/test_startup_imports.py | 42 ++++++++++++++++++ 11 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 test/test_utils/test_startup_imports.py diff --git a/CLAUDE.md b/CLAUDE.md index e496b233..4e65c296 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,6 +103,7 @@ ruff check pybreeze/ # before committing non-trivia - Plugin API: `register_programming_language()` / `register_natural_language()` from `je_editor.plugins` - A QAction built for a menu must be kept alive: store it on the main window or give it the menu as its parent. A menu does not own the actions added to it, so one held only by a local variable is deleted when the builder returns and its entry disappears - A context menu or dialog built on each use with a parent (`QMenu(self)`, `SomeDialog(self)`) is deleted once `exec()` returns (`deleteLater()`, or `WA_DeleteOnClose` for a message box): its parent keeps it otherwise, one more per use +- Import `je_auto_control` only where it is used, never at the top of a module the IDE loads as it starts: it makes the process system DPI aware as it imports, which keeps Qt from making the IDE per-monitor aware (`test_startup_imports.py` fails on it) - Delete unused code immediately — no dead imports, unreachable branches, commented-out blocks, or `_old_` prefixes - Follow PEP 8 and standard Pythonic practice; `ruff` is the arbiter diff --git a/architecture_explore.md b/architecture_explore.md index fd6d1f65..5c7d875e 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -173,14 +173,14 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 5.1 `automation_menu_factory.py` — 選單工廠 -`build_automation_menu(ui, spec)` 依一份 `AutomationMenu` 描述組出標準自動化子選單:`Run` 子選單(`RunAction` 列表)/ `Help`(`HelpLink` 列表,文件+GitHub,開內嵌瀏覽器分頁)/ `Project`(建立範本目錄)/ GUI 分頁,每一段各由一個小函式建(`_add_run_menu` 等),沒有項目的段落不建。三個描述都是 frozen dataclass。六個自動化模組全部靠它,`build_*_menu.py` 只剩一份 `AutomationMenu(...)`。每個 QAction 都以它所在的選單為 parent,由 Qt 持有;AutoControl 額外的 `Record` 子選單也一樣;它的停止錄製不論前面是哪個分頁都會停,把動作以 AutoControl 執行器讀的 JSON 插在編輯分頁的游標處(沒有編輯分頁就放剪貼簿),沒錄到東西就告知。 +`build_automation_menu(ui, spec)` 依一份 `AutomationMenu` 描述組出標準自動化子選單:`Run` 子選單(`RunAction` 列表)/ `Help`(`HelpLink` 列表,文件+GitHub,開內嵌瀏覽器分頁)/ `Project`(建立範本目錄)/ GUI 分頁(`gui_widget_factory`,選到才呼叫,套件可以到那時才 import 它的 GUI),每一段各由一個小函式建(`_add_run_menu` 等),沒有項目的段落不建。三個描述都是 frozen dataclass。六個自動化模組全部靠它,`build_*_menu.py` 只剩一份 `AutomationMenu(...)`。每個 QAction 都以它所在的選單為 parent,由 Qt 持有;AutoControl 額外的 `Record` 子選單也一樣;它的停止錄製不論前面是哪個分頁都會停,把動作以 AutoControl 執行器讀的 JSON 插在編輯分頁的游標處(沒有編輯分頁就放剪貼簿),沒錄到東西就告知。`je_auto_control` 一 import 就把行程設成 system DPI aware,所以這個模組只在用到時才 import 它(`_auto_control()`、`_autocontrol_gui()`):跟著選單在應用程式建立前 import,Qt 就設不成 per-monitor v2(每次啟動都警告 `SetProcessDpiAwarenessContext() failed`),在縮放比例跟主螢幕不同的螢幕上,Windows 把整個 IDE 當點陣圖拉伸。`test_startup_imports.py` 守著這點。 `safe_create_project(ui, import_name)` 回傳延遲 import 的 closure:專案建在 IDE 開著的資料夾(`working_dir`,沒開就用行程的工作目錄);套件的資料夾(`create_project_dir` 的 `parent_name` 預設值)已存在時先問(預設否),因為各套件一律覆寫範本檔;模組沒裝、寫入失敗都跳警告並記 log,成功時說出建在哪裡。 | 選單 | 文件 | GUI 分頁 | |---|---|---| | APITestka | apitestka.readthedocs.io | `APITestkaWidget` | -| AutoControl | autocontrol.readthedocs.io | `AutoControlGUIWidget` | +| AutoControl | autocontrol.readthedocs.io | `AutoControlGUIWidget`(開分頁時才 import) | | WebRunner | webrunner.readthedocs.io | — | | LoadDensity | loaddensity.readthedocs.io | `LoadDensityWidget` | | FileAutomation | fileautomation.readthedocs.io | — | @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 147 個 `test_*.py`、2682 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2686 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 316ff6bc..60d52336 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -841,3 +841,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `.coveragerc`, `CLAUDE.md`, `architecture_explore.md` - **Checks**: the start-theme and Stop All tests with and without the setting: `main_ui.py` 0 % → 69 %, `build_menubar.py` 0 % → 100 %; without `COVERAGE_FILE` set, the children (working directory a temporary folder) still write where the parent combines. The XML report has the same file names and `` as before. The full run with it (Python 3.11, coverage 7.14.3, pytest-cov 7.1.0): 2667 passed, 15 skipped, exit 0, in 13 minutes; statements 96.4 %, with branches 94.9 %, `editor_main` 90 %, `menu` 97 %; `architecture_explore.md` quoted 93 %, 91 %, 72 % and 85 %, measured earlier without the children, and now quotes these. - **Open items**: none. + +## U-20260925-101 · 2026-09-25 · The IDE is per-monitor DPI aware again: AutoControl is imported when used · #fix #startup #autocontrol + +- **What**: every start printed `SetProcessDpiAwarenessContext() failed: Access is denied` and the IDE ran system DPI aware instead of Qt 6's per-monitor v2, so Windows stretched it as a bitmap on a screen scaled differently from the main one. `je_auto_control/windows/screen/win32_screen.py` calls `SetProcessDPIAware()` as it imports, and `build_autocontrol_menu.py` imported the package (and its GUI) at the top, so it ran as the menus were imported, before `start_editor` created the application. Checked with the Windows platform plugin and no window: Qt alone gives per-monitor v2; `je_auto_control` or the main-window module imported first gives system aware and the warning. +- **Change**: `AutomationMenu.gui_widget_class` is now `gui_widget_factory`, a callable run each time the GUI entry is chosen (a class still fits). The AutoControl menu imports the package in `_auto_control()` and its GUI in `_autocontrol_gui()`; Record and Stop Record go through the first. The other two GUI entries are unchanged for now. +- **Tests**: `test_startup_imports.py` (new) starts the real main window in a child and fails if `je_auto_control` was imported or (on Windows) the process is DPI aware before Qt sets it; both failed before the change (awareness 1, package loaded). `test_autocontrol_record.py` patches the package itself, and gains tests that Record starts AutoControl's recording and that the GUI entry opens AutoControl's GUI in a tab. +- **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py`, `.../auto_control_menu/build_autocontrol_menu.py`, `.../load_density_menu/build_load_density_menu.py`, `.../api_testka_menu/build_api_testka_menu.py`, `test/test_utils/test_startup_imports.py` (new), `test/test_utils/test_autocontrol_record.py`, `test/test_utils/test_automation_menu_factory.py`, `CLAUDE.md` (Conventions), `architecture_explore.md` (§5.1, test count) +- **Checks**: the same check with the main-window module imported first now gives per-monitor v2 and no warning. The menu, record and startup-import tests pass (12). `test/test_utils` collects 2686 tests in 148 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index ffef5c8b..bc2d9cd9 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-101 | 2026-09-25 | The IDE is per-monitor DPI aware again: AutoControl is imported when used | #fix #startup #autocontrol | [2026-09-c](2026-09-c.md) | | U-20260925-100 | 2026-09-25 | Coverage counts the tests that start the real main window in a child | #ci #test #coverage | [2026-09-c](2026-09-c.md) | | U-20260925-99 | 2026-09-25 | The JupyterLab tab test no longer crashes the test run on exit | #test #fix #jupyter | [2026-09-c](2026-09-c.md) | | U-20260925-98 | 2026-09-25 | Installing JupyterLab for the tab is tested | #test #jupyter | [2026-09-c](2026-09-c.md) | @@ -503,4 +504,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 100 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 101 | diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py index 7bdaea0f..996f7d01 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py @@ -37,6 +37,6 @@ def set_apitestka_menu(ui_we_want_to_set: PyBreezeMainWindow): ), create_project=safe_create_project(ui_we_want_to_set, "je_api_testka"), create_project_label_key="apitestka_create_project_label", - gui_widget_class=APITestkaWidget, + gui_widget_factory=APITestkaWidget, gui_label="APITestka GUI", )) diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/auto_control_menu/build_autocontrol_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/auto_control_menu/build_autocontrol_menu.py index ca5e91dc..7d83ab05 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/auto_control_menu/build_autocontrol_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/auto_control_menu/build_autocontrol_menu.py @@ -1,12 +1,11 @@ from __future__ import annotations import json +from types import ModuleType from typing import TYPE_CHECKING -import je_auto_control from PySide6.QtGui import QAction, QGuiApplication, QTextCharFormat -from PySide6.QtWidgets import QMessageBox -from je_auto_control.gui.main_widget import AutoControlGUIWidget +from PySide6.QtWidgets import QMessageBox, QWidget from je_editor import EditorWidget, language_wrapper from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict @@ -23,6 +22,27 @@ ) +def _auto_control() -> ModuleType: + """The ``je_auto_control`` package, imported the first time it is used. + + It makes the process system DPI aware as it imports. Imported with the + menus, before the application existed, it kept Qt from making the IDE + per-monitor aware, and Windows stretched the window as a bitmap on a screen + scaled differently from the main one. + """ + import je_auto_control + return je_auto_control + + +def _autocontrol_gui() -> QWidget: + from je_auto_control.gui.main_widget import AutoControlGUIWidget + return AutoControlGUIWidget() + + +def _start_recording() -> None: + _auto_control().record() + + def set_autocontrol_menu(ui_we_want_to_set: PyBreezeMainWindow): menu = build_automation_menu(ui_we_want_to_set, AutomationMenu( label_key="autocontrol_menu_label", @@ -43,7 +63,7 @@ def set_autocontrol_menu(ui_we_want_to_set: PyBreezeMainWindow): ), create_project=safe_create_project(ui_we_want_to_set, "je_auto_control"), create_project_label_key="autocontrol_create_project_label", - gui_widget_class=AutoControlGUIWidget, + gui_widget_factory=_autocontrol_gui, gui_label="AutoControl GUI", )) @@ -52,7 +72,7 @@ def set_autocontrol_menu(ui_we_want_to_set: PyBreezeMainWindow): record_menu = menu.addMenu(lang.get("autocontrol_record_menu_label")) record_action = QAction(lang.get("autocontrol_record_start_label"), record_menu) - record_action.triggered.connect(je_auto_control.record) + record_action.triggered.connect(_start_recording) record_menu.addAction(record_action) stop_record_action = QAction(lang.get("autocontrol_record_stop_label"), record_menu) @@ -70,7 +90,7 @@ def stop_record(editor_instance: PyBreezeMainWindow) -> None: or on the clipboard when there is none. When nothing was recorded -- or recording was never started -- the user is told, instead of getting "None". """ - actions = je_auto_control.stop_record() + actions = _auto_control().stop_record() lang = language_wrapper.language_word_dict title = lang.get("autocontrol_record_menu_label") if not actions: diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py b/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py index 14b3b65b..31316506 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py @@ -38,15 +38,16 @@ class AutomationMenu: """Everything one automation package's menu holds. A submenu is built only when it has entries; the project entry needs both - ``create_project`` and its label key, and the GUI entry both the widget - class and its label. + ``create_project`` and its label key, and the GUI entry both + ``gui_widget_factory`` (called each time the entry is chosen, so a package + can import its GUI only then) and its label. """ label_key: str run_actions: tuple[RunAction, ...] = () help_links: tuple[HelpLink, ...] = () create_project: Callable[[], None] | None = None create_project_label_key: str | None = None - gui_widget_class: type[QWidget] | None = None + gui_widget_factory: Callable[[], QWidget] | None = None gui_label: str | None = None @@ -70,8 +71,8 @@ def build_automation_menu(ui: PyBreezeMainWindow, spec: AutomationMenu) -> QMenu add_help_menu(ui, menu, spec.help_links) if spec.create_project and spec.create_project_label_key: _add_project_menu(menu, spec.create_project, spec.create_project_label_key) - if spec.gui_widget_class and spec.gui_label: - _add_gui_action(ui, menu, spec.gui_widget_class, spec.gui_label) + if spec.gui_widget_factory and spec.gui_label: + _add_gui_action(ui, menu, spec.gui_widget_factory, spec.gui_label) return menu @@ -106,10 +107,10 @@ def _add_project_menu(menu: QMenu, create_project: Callable[[], None], label_key def _add_gui_action( - ui: PyBreezeMainWindow, menu: QMenu, widget_class: type[QWidget], label: str) -> None: + ui: PyBreezeMainWindow, menu: QMenu, widget_factory: Callable[[], QWidget], label: str) -> None: action = QAction(label, menu) action.triggered.connect( - lambda checked=False: ui.tab_widget.addTab(widget_class(), label) + lambda checked=False: ui.tab_widget.addTab(widget_factory(), label) ) menu.addAction(action) diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py index 5bcf05c9..1f6ec031 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py @@ -38,6 +38,6 @@ def set_load_density_menu(ui_we_want_to_set: PyBreezeMainWindow): ), create_project=safe_create_project(ui_we_want_to_set, "je_load_density"), create_project_label_key="load_density_create_project_label", - gui_widget_class=LoadDensityWidget, + gui_widget_factory=LoadDensityWidget, gui_label="LoadDensity GUI", )) diff --git a/test/test_utils/test_autocontrol_record.py b/test/test_utils/test_autocontrol_record.py index dd8ff832..81940170 100644 --- a/test/test_utils/test_autocontrol_record.py +++ b/test/test_utils/test_autocontrol_record.py @@ -6,6 +6,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +import je_auto_control import pytest from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox, QPlainTextEdit, QTabWidget, QWidget @@ -49,7 +50,7 @@ def stop(): stopped.append(True) return actions - monkeypatch.setattr(record_menu.je_auto_control, "stop_record", stop) + monkeypatch.setattr(je_auto_control, "stop_record", stop) return stopped @@ -87,3 +88,44 @@ def test_nothing_recorded_says_so_and_inserts_nothing(window, monkeypatch, nothi # It used to insert the text "None". assert tab.code_edit.toPlainText() == "" assert window.told + + +class TestTheMenu: + """The entries that reach AutoControl import it only when chosen (see test_startup_imports.py).""" + + @staticmethod + def _menu(window) -> dict[str, object]: + from PySide6.QtWidgets import QMenu + + window.automation_menu = QMenu() + record_menu.set_autocontrol_menu(window) + (autocontrol,) = [action.menu() for action in window.automation_menu.actions()] + entries = {} + for action in autocontrol.actions(): + entries[action.text()] = action + if action.menu() is not None: + entries.update({sub.text(): sub for sub in action.menu().actions()}) + return entries + + def test_record_starts_autocontrol_recording(self, window, monkeypatch): + from je_editor import language_wrapper + + started: list = [] + monkeypatch.setattr(je_auto_control, "record", lambda: started.append(True)) + + self._menu(window)[language_wrapper.language_word_dict.get("autocontrol_record_start_label")].trigger() + + assert started == [True] + + def test_the_gui_entry_opens_autocontrols_gui_in_a_tab(self, window, monkeypatch): + from je_auto_control.gui import main_widget + + class AutoControlGUI(QWidget): + """Stands in for AutoControl's own GUI.""" + + monkeypatch.setattr(main_widget, "AutoControlGUIWidget", AutoControlGUI) + + self._menu(window)["AutoControl GUI"].trigger() + + assert isinstance(window.tab_widget.widget(0), AutoControlGUI) + assert window.tab_widget.tabText(0) == "AutoControl GUI" diff --git a/test/test_utils/test_automation_menu_factory.py b/test/test_utils/test_automation_menu_factory.py index 5db1c790..92e25099 100644 --- a/test/test_utils/test_automation_menu_factory.py +++ b/test/test_utils/test_automation_menu_factory.py @@ -96,7 +96,7 @@ def test_project_and_gui_actions_survive_and_answer(window): "run_label", create_project=lambda: created.append(True), create_project_label_key="project_label", - gui_widget_class=QWidget, gui_label="GUI", + gui_widget_factory=QWidget, gui_label="GUI", )) gc.collect() diff --git a/test/test_utils/test_startup_imports.py b/test/test_utils/test_startup_imports.py new file mode 100644 index 00000000..e85205cc --- /dev/null +++ b/test/test_utils/test_startup_imports.py @@ -0,0 +1,42 @@ +"""What starting the IDE leaves alone: the DPI awareness Qt sets, and packages nobody has used yet.""" +from __future__ import annotations + +import sys + +import pytest + +from test_utils.started_window import run_started_window + +_WHAT_THE_START_LOADED = """ +import ctypes +awareness = None +if sys.platform == "win32": + user32 = ctypes.windll.user32 + user32.GetThreadDpiAwarenessContext.restype = ctypes.c_void_p + user32.GetAwarenessFromDpiAwarenessContext.argtypes = [ctypes.c_void_p] + awareness = user32.GetAwarenessFromDpiAwarenessContext(user32.GetThreadDpiAwarenessContext()) +result = { + "awareness": awareness, + "loaded": sorted(name for name in ("je_auto_control",) if name in sys.modules), +} +""" + + +@pytest.fixture(scope="module") +def started(tmp_path_factory) -> dict: + return run_started_window(tmp_path_factory.mktemp("started"), _WHAT_THE_START_LOADED) + + +@pytest.mark.skipif(sys.platform != "win32", reason="DPI awareness is a Windows process setting") +def test_the_dpi_awareness_is_left_to_qt(started): + # je_auto_control makes the process system DPI aware as it imports. Imported + # with the menus, before the application existed, it left Qt unable to set + # per-monitor awareness ("SetProcessDpiAwarenessContext() failed" at every + # start): Windows stretched the IDE as a bitmap on a screen scaled + # differently from the main one. The offscreen platform sets none, so the + # process is still unaware (0) here. + assert started["awareness"] == 0 + + +def test_the_autocontrol_package_is_loaded_when_used(started): + assert started["loaded"] == [] From 98b22d99285b73ee3a6ec85468f00bff9f30e27f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 15:55:45 +0800 Subject: [PATCH 153/166] Load the automation GUIs and the SSH client when first opened --- CLAUDE.md | 2 +- architecture_explore.md | 10 +++--- docs/updates/2026-09-c.md | 9 ++++++ docs/updates/README.md | 3 +- .../api_testka_menu/build_api_testka_menu.py | 12 +++++-- .../build_load_density_menu.py | 13 ++++++-- pybreeze/pybreeze_ui/menu/tools/tools_menu.py | 13 ++++++-- .../test_automation_menu_factory.py | 32 +++++++++++++++++++ test/test_utils/test_startup_imports.py | 11 +++++-- 9 files changed, 87 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4e65c296..ad3f7c84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,7 +103,7 @@ ruff check pybreeze/ # before committing non-trivia - Plugin API: `register_programming_language()` / `register_natural_language()` from `je_editor.plugins` - A QAction built for a menu must be kept alive: store it on the main window or give it the menu as its parent. A menu does not own the actions added to it, so one held only by a local variable is deleted when the builder returns and its entry disappears - A context menu or dialog built on each use with a parent (`QMenu(self)`, `SomeDialog(self)`) is deleted once `exec()` returns (`deleteLater()`, or `WA_DeleteOnClose` for a message box): its parent keeps it otherwise, one more per use -- Import `je_auto_control` only where it is used, never at the top of a module the IDE loads as it starts: it makes the process system DPI aware as it imports, which keeps Qt from making the IDE per-monitor aware (`test_startup_imports.py` fails on it) +- Import `je_auto_control` only where it is used, never at the top of a module the IDE loads as it starts: it makes the process system DPI aware as it imports, which keeps Qt from making the IDE per-monitor aware. The automation packages' GUIs and the SSH client (paramiko) are likewise imported by the entry that opens them, which keeps almost two seconds off the start; `test_startup_imports.py` fails when one of them is imported as the IDE starts - Delete unused code immediately — no dead imports, unreachable branches, commented-out blocks, or `_old_` prefixes - Follow PEP 8 and standard Pythonic practice; `ruff` is the arbiter diff --git a/architecture_explore.md b/architecture_explore.md index 5c7d875e..2e9ccbe2 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -173,16 +173,16 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) ### 5.1 `automation_menu_factory.py` — 選單工廠 -`build_automation_menu(ui, spec)` 依一份 `AutomationMenu` 描述組出標準自動化子選單:`Run` 子選單(`RunAction` 列表)/ `Help`(`HelpLink` 列表,文件+GitHub,開內嵌瀏覽器分頁)/ `Project`(建立範本目錄)/ GUI 分頁(`gui_widget_factory`,選到才呼叫,套件可以到那時才 import 它的 GUI),每一段各由一個小函式建(`_add_run_menu` 等),沒有項目的段落不建。三個描述都是 frozen dataclass。六個自動化模組全部靠它,`build_*_menu.py` 只剩一份 `AutomationMenu(...)`。每個 QAction 都以它所在的選單為 parent,由 Qt 持有;AutoControl 額外的 `Record` 子選單也一樣;它的停止錄製不論前面是哪個分頁都會停,把動作以 AutoControl 執行器讀的 JSON 插在編輯分頁的游標處(沒有編輯分頁就放剪貼簿),沒錄到東西就告知。`je_auto_control` 一 import 就把行程設成 system DPI aware,所以這個模組只在用到時才 import 它(`_auto_control()`、`_autocontrol_gui()`):跟著選單在應用程式建立前 import,Qt 就設不成 per-monitor v2(每次啟動都警告 `SetProcessDpiAwarenessContext() failed`),在縮放比例跟主螢幕不同的螢幕上,Windows 把整個 IDE 當點陣圖拉伸。`test_startup_imports.py` 守著這點。 +`build_automation_menu(ui, spec)` 依一份 `AutomationMenu` 描述組出標準自動化子選單:`Run` 子選單(`RunAction` 列表)/ `Help`(`HelpLink` 列表,文件+GitHub,開內嵌瀏覽器分頁)/ `Project`(建立範本目錄)/ GUI 分頁(`gui_widget_factory`,選到才呼叫,套件可以到那時才 import 它的 GUI),每一段各由一個小函式建(`_add_run_menu` 等),沒有項目的段落不建。三個描述都是 frozen dataclass。六個自動化模組全部靠它,`build_*_menu.py` 只剩一份 `AutomationMenu(...)`。每個 QAction 都以它所在的選單為 parent,由 Qt 持有;AutoControl 額外的 `Record` 子選單也一樣;它的停止錄製不論前面是哪個分頁都會停,把動作以 AutoControl 執行器讀的 JSON 插在編輯分頁的游標處(沒有編輯分頁就放剪貼簿),沒錄到東西就告知。`je_auto_control` 一 import 就把行程設成 system DPI aware,所以這個模組只在用到時才 import 它(`_auto_control()`、`_autocontrol_gui()`):跟著選單在應用程式建立前 import,Qt 就設不成 per-monitor v2(每次啟動都警告 `SetProcessDpiAwarenessContext() failed`),在縮放比例跟主螢幕不同的螢幕上,Windows 把整個 IDE 當點陣圖拉伸。`test_startup_imports.py` 守著這點,也守著三個 GUI 與 SSH 用到時才 import:跟著選單一起 import 時,光是 import 主視窗模組就要 6.45 秒(中位數),現在 4.65 秒。 `safe_create_project(ui, import_name)` 回傳延遲 import 的 closure:專案建在 IDE 開著的資料夾(`working_dir`,沒開就用行程的工作目錄);套件的資料夾(`create_project_dir` 的 `parent_name` 預設值)已存在時先問(預設否),因為各套件一律覆寫範本檔;模組沒裝、寫入失敗都跳警告並記 log,成功時說出建在哪裡。 | 選單 | 文件 | GUI 分頁 | |---|---|---| -| APITestka | apitestka.readthedocs.io | `APITestkaWidget` | +| APITestka | apitestka.readthedocs.io | `APITestkaWidget`(開分頁時才 import) | | AutoControl | autocontrol.readthedocs.io | `AutoControlGUIWidget`(開分頁時才 import) | | WebRunner | webrunner.readthedocs.io | — | -| LoadDensity | loaddensity.readthedocs.io | `LoadDensityWidget` | +| LoadDensity | loaddensity.readthedocs.io | `LoadDensityWidget`(開分頁時才 import:套件會帶進 locust 與 gevent) | | FileAutomation | fileautomation.readthedocs.io | — | | MailThunder | mailthunder.readthedocs.io | — | @@ -195,7 +195,7 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True) 這是全專案設計最乾淨的一塊。三張表把 20 個工具的「建構」「分頁開啟」「dock 開啟」完全解耦: -- `_WIDGET_FACTORIES: dict[str, Callable]` — widget key → 建構 lambda +- `_WIDGET_FACTORIES: dict[str, Callable]` — widget key → 建構 lambda;SSH 的經 `_ssh_widget()`,第一次開才 import(paramiko 與 cryptography 約佔啟動的六分之一秒) - `_TAB_ACTIONS: tuple[...]` — (widget key, 主視窗屬性, 選單屬性, action 語言鍵, 分頁標籤鍵) - `_DOCK_ACTIONS` / `_DOCK_TITLES` — 同一組 widget 也能開成右側 dock(`closing.AskingDock`:關 dock 前先問 widget 的 `may_close()`,有未存變更的提示詞與架構圖編輯器不會被 dock 的關閉鈕直接丟掉);AI 類的 dock 放進 JEditor Dock 選單原有的 AI 子選單(`dock_ai_menu`),沒有才自己建一個 @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2686 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2688 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 60d52336..4ddbe4f3 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -850,3 +850,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/automation_menu_factory.py`, `.../auto_control_menu/build_autocontrol_menu.py`, `.../load_density_menu/build_load_density_menu.py`, `.../api_testka_menu/build_api_testka_menu.py`, `test/test_utils/test_startup_imports.py` (new), `test/test_utils/test_autocontrol_record.py`, `test/test_utils/test_automation_menu_factory.py`, `CLAUDE.md` (Conventions), `architecture_explore.md` (§5.1, test count) - **Checks**: the same check with the main-window module imported first now gives per-monitor v2 and no warning. The menu, record and startup-import tests pass (12). `test/test_utils` collects 2686 tests in 148 files. - **Open items**: none. + +## U-20260925-102 · 2026-09-25 · The IDE starts about 1.8 s sooner: the automation GUIs and SSH load when opened · #perf #startup + +- **What**: `-X importtime` on the main-window module put the menu bar at 1.2–1.4 s of 4.3–5.0 s of imports, nearly all of it three packages no start needs: the Load Density GUI (`je_load_density`, which brings locust and gevent, about 0.45–0.53 s), AutoControl (0.33–0.38 s, loaded as it is used since U-20260925-101), the APITestka GUI (0.13–0.15 s), and the SSH client's paramiko and cryptography through the Tools menu (0.16–0.19 s). The rest of the start is JEditor's own imports (about 3 s). +- **Change**: the Load Density and APITestka menus pass `_load_density_gui()` / `_api_testka_gui()` as their `gui_widget_factory`, and the Tools menu's SSH entry goes through `_ssh_widget()`: each imports its widget when the entry is chosen. Nothing else changes; locust still starts without gevent's patching, since `main_ui.py` sets `LOCUST_SKIP_MONKEY_PATCH` for the whole process. +- **Tests**: `test_startup_imports.py` also fails when `je_load_density`, `locust`, `je_api_testka` or `paramiko` is loaded once the real main window is built (it failed on all four before the change). `test_automation_menu_factory.py` checks that the Load Density and APITestka GUI entries still open their GUI in a tab, through a stand-in module in `sys.modules`, so the test process never imports locust. The Tools-tab test already opens the SSH client. +- **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py`, `.../api_testka_menu/build_api_testka_menu.py`, `pybreeze/pybreeze_ui/menu/tools/tools_menu.py`, `test/test_utils/test_startup_imports.py`, `test/test_utils/test_automation_menu_factory.py`, `CLAUDE.md` (Conventions), `architecture_explore.md` (§5.1, the Tools registry, test count) +- **Checks**: importing the main-window module, before (706f21d) and after, alternated six times each: median 6.45 s → 4.65 s, fastest 6.11 s → 4.42 s; the menu bar's share is now about 0.2 s. The startup-import, factory, AutoControl record and Tools-tab tests pass (31). `test/test_utils` collects 2688 tests in 148 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index bc2d9cd9..440b35e4 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-102 | 2026-09-25 | The IDE starts about 1.8 s sooner: the automation GUIs and SSH load when opened | #perf #startup | [2026-09-c](2026-09-c.md) | | U-20260925-101 | 2026-09-25 | The IDE is per-monitor DPI aware again: AutoControl is imported when used | #fix #startup #autocontrol | [2026-09-c](2026-09-c.md) | | U-20260925-100 | 2026-09-25 | Coverage counts the tests that start the real main window in a child | #ci #test #coverage | [2026-09-c](2026-09-c.md) | | U-20260925-99 | 2026-09-25 | The JupyterLab tab test no longer crashes the test run on exit | #test #fix #jupyter | [2026-09-c](2026-09-c.md) | @@ -504,4 +505,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 101 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 102 | diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py index 996f7d01..29849764 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py @@ -2,13 +2,13 @@ from typing import TYPE_CHECKING -from je_api_testka.gui.main_widget import APITestkaWidget - from pybreeze.pybreeze_ui.menu.automation_menu.automation_menu_factory import ( AutomationMenu, HelpLink, RunAction, build_automation_menu, safe_create_project ) if TYPE_CHECKING: + from PySide6.QtWidgets import QWidget + from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow from pybreeze.extend.process_executor.api_testka.api_testka_process import ( @@ -17,6 +17,12 @@ ) +def _api_testka_gui() -> QWidget: + # Imported when its tab opens, not with the menus as the IDE starts + from je_api_testka.gui.main_widget import APITestkaWidget + return APITestkaWidget() + + def set_apitestka_menu(ui_we_want_to_set: PyBreezeMainWindow): build_automation_menu(ui_we_want_to_set, AutomationMenu( label_key="apitestka_menu_label", @@ -37,6 +43,6 @@ def set_apitestka_menu(ui_we_want_to_set: PyBreezeMainWindow): ), create_project=safe_create_project(ui_we_want_to_set, "je_api_testka"), create_project_label_key="apitestka_create_project_label", - gui_widget_factory=APITestkaWidget, + gui_widget_factory=_api_testka_gui, gui_label="APITestka GUI", )) diff --git a/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py b/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py index 1f6ec031..92abc4dd 100644 --- a/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py +++ b/pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py @@ -2,13 +2,13 @@ from typing import TYPE_CHECKING -from je_load_density.gui.main_widget import LoadDensityWidget - from pybreeze.pybreeze_ui.menu.automation_menu.automation_menu_factory import ( AutomationMenu, HelpLink, RunAction, build_automation_menu, safe_create_project ) if TYPE_CHECKING: + from PySide6.QtWidgets import QWidget + from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow from pybreeze.extend.process_executor.load_density.load_density_process import ( @@ -17,6 +17,13 @@ ) +def _load_density_gui() -> QWidget: + # Imported when its tab opens: the package brings locust and gevent, about + # half a second of the IDE's start + from je_load_density.gui.main_widget import LoadDensityWidget + return LoadDensityWidget() + + def set_load_density_menu(ui_we_want_to_set: PyBreezeMainWindow): build_automation_menu(ui_we_want_to_set, AutomationMenu( label_key="load_density_menu_label", @@ -38,6 +45,6 @@ def set_load_density_menu(ui_we_want_to_set: PyBreezeMainWindow): ), create_project=safe_create_project(ui_we_want_to_set, "je_load_density"), create_project_label_key="load_density_create_project_label", - gui_widget_factory=LoadDensityWidget, + gui_widget_factory=_load_density_gui, gui_label="LoadDensity GUI", )) diff --git a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py index a8b820ec..b368b96e 100644 --- a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py +++ b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py @@ -9,7 +9,6 @@ from je_editor import jeditor_logger from pybreeze.pybreeze_ui.closing import AskingDock -from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_main_widget import SSHMainWidget from pybreeze.pybreeze_ui.connect_gui.url.ai_code_review_gui import AICodeReviewClient from pybreeze.pybreeze_ui.diagram_editor.diagram_editor_widget import DiagramEditorWidget from pybreeze.pybreeze_ui.extend_ai_gui.code_review.cot_code_review_gui import CoTCodeReviewGUI @@ -32,16 +31,26 @@ from pybreeze.pybreeze_ui.tools_gui.url_builder_gui import UrlBuilderGUI if TYPE_CHECKING: + from PySide6.QtWidgets import QWidget + from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow # --------------------------------------------------------------------------- # Widget registry # --------------------------------------------------------------------------- + +def _ssh_widget() -> QWidget: + # Imported when the SSH client first opens: paramiko and cryptography take + # about a sixth of a second of the IDE's start + from pybreeze.pybreeze_ui.connect_gui.ssh.ssh_main_widget import SSHMainWidget + return SSHMainWidget() + + # Widget key -> factory taking the main window. Shared by the Tools-menu tab # actions and the dock actions so each widget's constructor is written once. _WIDGET_FACTORIES: dict[str, Callable[[PyBreezeMainWindow], object]] = { - "SSH": lambda _win: SSHMainWidget(), + "SSH": lambda _win: _ssh_widget(), "AICodeReview": lambda _win: AICodeReviewClient(), "CoTPromptEditor": lambda _win: CoTPromptEditor(), "CoTCodeReview": lambda _win: CoTCodeReviewGUI(), diff --git a/test/test_utils/test_automation_menu_factory.py b/test/test_utils/test_automation_menu_factory.py index 92e25099..cdb1ba86 100644 --- a/test/test_utils/test_automation_menu_factory.py +++ b/test/test_utils/test_automation_menu_factory.py @@ -124,3 +124,35 @@ def test_every_create_project_names_a_package_that_exists(): assert names, "no create-project entries found" assert [name for name in names if importlib.util.find_spec(name) is None] == [] + + +@pytest.mark.parametrize("menu_module, set_menu, gui_module, class_name, label", [ + ("load_density_menu.build_load_density_menu", "set_load_density_menu", + "je_load_density.gui.main_widget", "LoadDensityWidget", "LoadDensity GUI"), + ("api_testka_menu.build_api_testka_menu", "set_apitestka_menu", + "je_api_testka.gui.main_widget", "APITestkaWidget", "APITestka GUI"), +]) +def test_a_gui_entry_imports_the_packages_gui_when_chosen( + window, monkeypatch, menu_module, set_menu, gui_module, class_name, label): + # A stand-in module, so the package is never imported here: Load Density's + # brings locust, which patches the whole process with gevent as it imports + import importlib + import sys + import types + + class PackageGUI(QWidget): + """Stands in for the package's own GUI.""" + + stand_in = types.ModuleType(gui_module) + setattr(stand_in, class_name, PackageGUI) + monkeypatch.setitem(sys.modules, gui_module, stand_in) + module = importlib.import_module(f"pybreeze.pybreeze_ui.menu.automation_menu.{menu_module}") + getattr(module, set_menu)(window) + gc.collect() + + package_menus = window.automation_menu.actions() + (gui_action,) = [action for action in package_menus[0].menu().actions() if action.text() == label] + gui_action.trigger() + + assert isinstance(window.tab_widget.widget(0), PackageGUI) + assert window.tab_widget.tabText(0) == label diff --git a/test/test_utils/test_startup_imports.py b/test/test_utils/test_startup_imports.py index e85205cc..80436daf 100644 --- a/test/test_utils/test_startup_imports.py +++ b/test/test_utils/test_startup_imports.py @@ -7,7 +7,12 @@ from test_utils.started_window import run_started_window -_WHAT_THE_START_LOADED = """ +# Imported by the entries that use them: together they took about a fifth of +# the IDE's start (the Load Density GUI brings locust and gevent; SSH brings +# paramiko and cryptography) +_UNUSED_YET = ("je_auto_control", "je_load_density", "locust", "je_api_testka", "paramiko") + +_WHAT_THE_START_LOADED = f"UNUSED_YET = {_UNUSED_YET!r}\n" + """ import ctypes awareness = None if sys.platform == "win32": @@ -17,7 +22,7 @@ awareness = user32.GetAwarenessFromDpiAwarenessContext(user32.GetThreadDpiAwarenessContext()) result = { "awareness": awareness, - "loaded": sorted(name for name in ("je_auto_control",) if name in sys.modules), + "loaded": sorted(name for name in UNUSED_YET if name in sys.modules), } """ @@ -38,5 +43,5 @@ def test_the_dpi_awareness_is_left_to_qt(started): assert started["awareness"] == 0 -def test_the_autocontrol_package_is_loaded_when_used(started): +def test_the_automation_packages_and_ssh_are_loaded_when_used(started): assert started["loaded"] == [] From 599f2f5f2ccacc450d28aaa424846b60701c3a1f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 15:56:25 +0800 Subject: [PATCH 154/166] Record JEditor's startup imports in progress.md --- progress.md | 1 + 1 file changed, 1 insertion(+) diff --git a/progress.md b/progress.md index 33792bcf..d03349bf 100644 --- a/progress.md +++ b/progress.md @@ -27,3 +27,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#116** [BLOCKED] Opening a file into an editor tab replaces the text it holds without asking: a click in the project tree (`project_treeview.clicked` -> `treeview_click`) and File > Open File both call `EditorWidget.open_an_file()` (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:262`), which checks only whether the file is open elsewhere, then `setPlainText`s it. A tab with a file loses the last seconds of typing (auto-save runs every 5 s); a new tab loses everything. Reproduced on je_editor 1.0.27 in the real window: an untitled tab's text was replaced, nothing asked. Needs JEditor to ask when the tab has unsaved edits (its `_is_modified`), or open into a new tab; the Sphinx UI Overview says what happens now. - **#117** [BLOCKED] JEditor's Variable Inspector (Tab > Tools Tab, Dock > Editor, and each editor tab's bottom panel) is always empty: `VariableModel` shows the `namespace` it is given, and nothing gives it one (`je_editor/pyside_ui/code/variable_inspector/inspector_gui.py:17`, built with none at `editor_widget.py:151` and `build_tab_tools_menu.py:103`; no code sets `.namespace`), on je_editor 1.0.27. The README no longer lists it among the editor's features; the Sphinx pages say it is empty. Needs JEditor to feed it from somewhere (the IPython console's namespace, or a paused debugger's), or to drop it. - **#118** [BLOCKED] A settings file whose `ui_style` is not a string stops the IDE from starting: `EditorMain.__init__` calls `startup_setting()` unguarded (`je_editor/pyside_ui/main_ui/main_editor.py:245`), which hands the value to qt_material's `apply_stylesheet`, and a number raises `TypeError` inside the window's constructor, before PyBreeze can do anything (reproduced on je_editor 1.0.27 with `{"ui_style": 42}` in `.jeditor/user_setting.json`: the window was never built). A theme name that does not exist only logs a warning. Needs JEditor to check the saved style's type, or to guard that call as `_restore_open_files_session` is guarded. +- **#119** [BLOCKED] Most of the IDE's start is JEditor importing two tools nobody has opened yet: `build_dock_menu.py:17,22` and `build_tab_tools_menu.py:9-10` (`je_editor/pyside_ui/main_ui/menu/`) import `ChatUI`, which brings `langchain_openai` and `openai` (2.0–2.5 s), and `IpythonWidget`, which brings IPython (about 1 s), at the top. With `-X importtime` on je_editor 1.0.27, `build_dock_menu` is 3.1–3.7 s of the 4.1–4.9 s it takes to import PyBreeze's main-window module. PyBreeze imports its own heavy GUIs (the automation packages', SSH) when they are first opened. Needs JEditor to import these two in the actions that open them. From c577dfb2949e88234a98739db012d3911ad28de2 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 16:07:19 +0800 Subject: [PATCH 155/166] Keep the IDE's own locust setting out of the processes it starts --- CLAUDE.md | 1 + architecture.md | 5 +-- architecture_explore.md | 6 ++-- docs/updates/2026-09-c.md | 9 +++++ docs/updates/README.md | 3 +- progress.md | 1 + pybreeze/pybreeze_ui/editor_main/main_ui.py | 9 ++++- .../jupyter_lab_gui/jupyter_lab_thread.py | 4 +-- pybreeze/utils/subprocess_util.py | 19 +++++++++-- test/test_utils/test_jupyter_lifecycle.py | 13 ++++++++ test/test_utils/test_startup_imports.py | 21 +++++++++++- test/test_utils/test_subprocess_util.py | 33 ++++++++++++++++++- 12 files changed, 111 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ad3f7c84..18e8c3c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,6 +103,7 @@ ruff check pybreeze/ # before committing non-trivia - Plugin API: `register_programming_language()` / `register_natural_language()` from `je_editor.plugins` - A QAction built for a menu must be kept alive: store it on the main window or give it the menu as its parent. A menu does not own the actions added to it, so one held only by a local variable is deleted when the builder returns and its entry disappears - A context menu or dialog built on each use with a parent (`QMenu(self)`, `SomeDialog(self)`) is deleted once `exec()` returns (`deleteLater()`, or `WA_DeleteOnClose` for a message box): its parent keeps it otherwise, one more per use +- A process the IDE starts gets `child_environment()` or `utf8_subprocess_env()` (`utils/subprocess_util.py`) as its `env`, never `os.environ` as it is: a variable the IDE sets for itself alone has the value `IDE_ONLY` and stays out (`LOCUST_SKIP_MONKEY_PATCH`, which a load test must not inherit) - Import `je_auto_control` only where it is used, never at the top of a module the IDE loads as it starts: it makes the process system DPI aware as it imports, which keeps Qt from making the IDE per-monitor aware. The automation packages' GUIs and the SSH client (paramiko) are likewise imported by the entry that opens them, which keeps almost two seconds off the start; `test_startup_imports.py` fails when one of them is imported as the IDE starts - Delete unused code immediately — no dead imports, unreachable branches, commented-out blocks, or `_old_` prefixes - Follow PEP 8 and standard Pythonic practice; `ruff` is the arbiter diff --git a/architecture.md b/architecture.md index 4c1e5f68..aeb1f090 100644 --- a/architecture.md +++ b/architecture.md @@ -81,8 +81,9 @@ python -m pybreeze → start_editor() → QApplication → open_main_window() → apply_stylesheet(theme) → showMaximized() → startup_setting() → exec() → os._exit() ``` -Before PySide6 is imported, `main_ui.py` sets `LOCUST_SKIP_MONKEY_PATCH=1` to keep LoadDensity's -gevent patching away from Qt. +Before PySide6 is imported, `main_ui.py` sets `LOCUST_SKIP_MONKEY_PATCH` (to `IDE_ONLY`, unless the user +set it) to keep locust's gevent patching away from Qt. The processes the IDE starts get +`subprocess_util.child_environment()`, which leaves it out: a load test needs the patching. **Run an automation script** diff --git a/architecture_explore.md b/architecture_explore.md index 2e9ccbe2..adae2fd5 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -81,7 +81,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE 4. `showMaximized()` → `startup_setting()` → `app.exec()` 5. 離開時以 `os._exit(ret)` 硬退出(避開 Qt 拆解殘留執行緒) -模組層級有一個副作用:`main_ui.py:8` 在匯入 PySide6 之前就設定 `LOCUST_SKIP_MONKEY_PATCH=1`,避免 LoadDensity 的 gevent monkey patch 破壞 Qt。 +模組層級有一個副作用:`main_ui.py` 在匯入 PySide6 之前就設定 `LOCUST_SKIP_MONKEY_PATCH`(值是 `subprocess_util.IDE_ONLY`;使用者自己設過就沿用),避免 locust 一 import 就對整個行程做的 gevent monkey patch 破壞 Qt(Load Density GUI、JEditor 行程內的 IPython console 都可能 import 它)。IDE 啟動的行程拿到的是 `child_environment()`,不帶這個值:負載測試要靠 patch 才能讓使用者同時跑,帶著它時 HttpUser 一個接一個跑,3 秒的測試跑了三分多鐘。 --- @@ -348,7 +348,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 | `app_dirs.py` | `pybreeze_data_dir()` → `~/.pybreeze`,所有持久化資料的單一位置,建立時為 `0700`(`DATA_DIR_MODE`);`pybreeze_data_path()` 只給路徑、不建立 | | `terminal_text.py` | 終端輸出的 escape 與控制字元:`strip_terminal_controls()`(CSI、OSC/DCS 等控制字串、nF、兩位元組 escape 剝除,backspace 套用,其餘 C0 丟掉)、`split_incomplete_escape()`(讀取切斷在 escape 中間時把尾巴留給下一次)、`split_unfinished_end()`(再加上它前面或最後的 `\r`)、`take_leading_backspaces()`(一段開頭的 backspace 留給畫面,擦掉前一段已經顯示的字,不越過行首)、`split_at_screen_clear()`(最後一個清除整個畫面的序列:`ESC [ 2J`、`ESC [ 3J`、`ESC c`,前後切開;`ESC [ J` 只清游標以下,shell 重畫提示字元時會送,不算)。SSH terminal 與執行視窗共用 | | `terminal_style.py` | SGR(`ESC [ … m`)讀成 `TextStyle`(frozen dataclass:前景、背景、粗體、斜體、底線、反白):`apply_sgr()`(16 色與亮色、`38;5;n` 256 色、`38;2;r;g;b` 24 位元色、各開關與 39/49 預設、0 重設;看不懂或格式錯的參數不改任何東西,超過 5 位數的參數略過,`int()` 不收超過 4300 位數)、`split_styled()`(文字在 SGR 處切段,每段帶它的樣式,其餘 escape 留給 `strip_terminal_controls()`)、`colour_rgb(colour, on_dark=)`(前 16 色用 VS Code 終端機的預設值,深色與淺色主題各一組,`terminal_view.style_format()` 依 view 背景的亮度挑;256 色的色塊、灰階與 24 位元色照 xterm)。只有 SSH terminal 用:執行視窗的程式寫到 pipe,不會上色 | -| `subprocess_util.py` | `utf8_subprocess_env()`(釘 `PYTHONIOENCODING`,解 Windows cp950 亂碼)、`no_window_creationflags()`(`CREATE_NO_WINDOW`,避免 GUI 程式彈出黑窗) | +| `subprocess_util.py` | `child_environment()`(`os.environ` 去掉值為 `IDE_ONLY` 的變數:IDE 只給自己設的)、`utf8_subprocess_env()`(以它為底再釘 `PYTHONIOENCODING`,解 Windows cp950 亂碼)、`no_window_creationflags()`(`CREATE_NO_WINDOW`,避免 GUI 程式彈出黑窗) | | `logging/logger.py` | `pybreeze_logger`(具名 logger,**不動 root logger**)+ `PyBreezeLogger(RotatingFileHandler)`:寫到 `~/.pybreeze/logs/PyBreeze.log`(`PYBREEZE_LOG_FILE` 可改),UTF-8、附加模式、每行帶行程編號,第一筆紀錄才開檔;只在開檔時輪替,門檻 `PYBREEZE_LOG_MAX_BYTES`(預設 100 MB);開不了檔就改寫 `os.devnull` 並警告一次。與 JEditor、FrontEngine 同一套做法(工作區 X-6) | | `exception/` | `ITEException` 為根的 17 個例外類別 + `exception_tags.py` 訊息常數;`error_templates.py` 把名稱以 `_error` 結尾的常數變成語言字典的 `error_text_<名稱>`(英文字典直接取常數本身) | | `network/url_validation.py` | `validate_url()`:先拒絕 `urlparse` 與 `urllib3` 讀出不同主機的 URL(反斜線、空白、控制字元,或兩者主機不同;`_check_one_reading`),再做 scheme 白名單、私有/迴環/link-local/reserved 阻擋、額外處理 CGNAT 與 NAT64 網段、IPv6 內嵌 IPv4 的偵測 | @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2688 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2693 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 4ddbe4f3..0de6f6c6 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -859,3 +859,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py`, `.../api_testka_menu/build_api_testka_menu.py`, `pybreeze/pybreeze_ui/menu/tools/tools_menu.py`, `test/test_utils/test_startup_imports.py`, `test/test_utils/test_automation_menu_factory.py`, `CLAUDE.md` (Conventions), `architecture_explore.md` (§5.1, the Tools registry, test count) - **Checks**: importing the main-window module, before (706f21d) and after, alternated six times each: median 6.45 s → 4.65 s, fastest 6.11 s → 4.42 s; the menu bar's share is now about 0.2 s. The startup-import, factory, AutoControl record and Tools-tab tests pass (31). `test/test_utils` collects 2688 tests in 148 files. - **Open items**: none. + +## U-20260925-103 · 2026-09-25 · Load tests the IDE starts are patched with gevent again · #fix #load-density #subprocess + +- **What**: `main_ui.py` sets `LOCUST_SKIP_MONKEY_PATCH` so that locust, which patches the whole process with gevent as it imports, cannot do it to the IDE (the Load Density GUI imports it there, and so can a user in JEditor's in-process IPython console). The variable went on to every process the IDE started, through `utf8_subprocess_env()` (the automation menus, Run with..., installs) and the JupyterLab server, whose kernels inherit it. Unpatched, locust's HttpUser blocks the whole process on each request: measured with 10 users against a local server answering in 0.3 s, a 3 s run made 98 requests patched, and with the variable ran one user at a time for over three minutes (607 requests) with `ConnectionAbortedError`s. FastHttpUser, je_load_density's default, is unaffected (96 and 92); its `http_user`, one of the templates its projects are created with, and any HttpUser script are. +- **Change**: `subprocess_util.IDE_ONLY` is the value of a variable the IDE sets for itself alone, and `child_environment()` is `os.environ` without every variable that has it; `utf8_subprocess_env()` starts from it, and the JupyterLab server is started with it. `main_ui.py` sets `LOCUST_SKIP_MONKEY_PATCH` to the value the user gave it, or `IDE_ONLY`. JEditor's Run Program, Run Debugger and shell start their processes with the IDE's environment as it is, which PyBreeze cannot change: recorded as `progress.md` #120. +- **Tests**: `test_subprocess_util.py` (a variable with the value is left out, a real child runs without it, one the user set is passed on); `test_startup_imports.py` (in the real main window, started with the variable unset: the IDE has it, a child's environment does not; it failed before, the child got `1`); `test_jupyter_lifecycle.py` (the server's environment leaves it out). +- **Files**: `pybreeze/utils/subprocess_util.py`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py`, the three test files, `CLAUDE.md` (Conventions), `architecture.md` (§4), `architecture_explore.md` (main window, `subprocess_util.py`, test count), `progress.md` (#120) +- **Checks**: from a process that had imported the IDE, the same HttpUser run started with `utf8_subprocess_env()` had gevent's patching and made 100 requests in 4.6 s. The subprocess, startup, JupyterLab and prthinker-contract tests pass (44, 14 skipped: no prthinker here). `test/test_utils` collects 2693 tests in 148 files. +- **Open items**: `progress.md` #120. diff --git a/docs/updates/README.md b/docs/updates/README.md index 440b35e4..98d28cef 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-103 | 2026-09-25 | Load tests the IDE starts are patched with gevent again | #fix #load-density #subprocess | [2026-09-c](2026-09-c.md) | | U-20260925-102 | 2026-09-25 | The IDE starts about 1.8 s sooner: the automation GUIs and SSH load when opened | #perf #startup | [2026-09-c](2026-09-c.md) | | U-20260925-101 | 2026-09-25 | The IDE is per-monitor DPI aware again: AutoControl is imported when used | #fix #startup #autocontrol | [2026-09-c](2026-09-c.md) | | U-20260925-100 | 2026-09-25 | Coverage counts the tests that start the real main window in a child | #ci #test #coverage | [2026-09-c](2026-09-c.md) | @@ -505,4 +506,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 102 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 103 | diff --git a/progress.md b/progress.md index d03349bf..69118151 100644 --- a/progress.md +++ b/progress.md @@ -28,3 +28,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#117** [BLOCKED] JEditor's Variable Inspector (Tab > Tools Tab, Dock > Editor, and each editor tab's bottom panel) is always empty: `VariableModel` shows the `namespace` it is given, and nothing gives it one (`je_editor/pyside_ui/code/variable_inspector/inspector_gui.py:17`, built with none at `editor_widget.py:151` and `build_tab_tools_menu.py:103`; no code sets `.namespace`), on je_editor 1.0.27. The README no longer lists it among the editor's features; the Sphinx pages say it is empty. Needs JEditor to feed it from somewhere (the IPython console's namespace, or a paused debugger's), or to drop it. - **#118** [BLOCKED] A settings file whose `ui_style` is not a string stops the IDE from starting: `EditorMain.__init__` calls `startup_setting()` unguarded (`je_editor/pyside_ui/main_ui/main_editor.py:245`), which hands the value to qt_material's `apply_stylesheet`, and a number raises `TypeError` inside the window's constructor, before PyBreeze can do anything (reproduced on je_editor 1.0.27 with `{"ui_style": 42}` in `.jeditor/user_setting.json`: the window was never built). A theme name that does not exist only logs a warning. Needs JEditor to check the saved style's type, or to guard that call as `_restore_open_files_session` is guarded. - **#119** [BLOCKED] Most of the IDE's start is JEditor importing two tools nobody has opened yet: `build_dock_menu.py:17,22` and `build_tab_tools_menu.py:9-10` (`je_editor/pyside_ui/main_ui/menu/`) import `ChatUI`, which brings `langchain_openai` and `openai` (2.0–2.5 s), and `IpythonWidget`, which brings IPython (about 1 s), at the top. With `-X importtime` on je_editor 1.0.27, `build_dock_menu` is 3.1–3.7 s of the 4.1–4.9 s it takes to import PyBreeze's main-window module. PyBreeze imports its own heavy GUIs (the automation packages', SSH) when they are first opened. Needs JEditor to import these two in the actions that open them. +- **#120** [BLOCKED] A locust script run with JEditor's Run Program, Run Debugger or shell runs its HttpUser users one at a time: those start their process with the IDE's own environment (`subprocess.Popen` with no `env`, `je_editor/pyside_ui/code/code_process/code_exec.py:99,194`, `.../shell_process/shell_exec.py:95`), which carries `LOCUST_SKIP_MONKEY_PATCH` (PyBreeze sets it so that locust cannot patch the IDE with gevent), and unpatched a 3 s test of 10 users took over three minutes. PyBreeze's own runs (the automation menus, Run with..., installs, the JupyterLab server) leave it out through `child_environment()`, which drops every variable whose value is `subprocess_util.IDE_ONLY`. Needs JEditor to let its host give the environment for the processes it starts, or to drop variables with that value. diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index 332a5b4d..544eccc7 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -5,7 +5,14 @@ from os import environ from pathlib import Path -environ["LOCUST_SKIP_MONKEY_PATCH"] = "1" +from pybreeze.utils.subprocess_util import IDE_ONLY + +# locust patches the whole process with gevent as it imports unless this is set, +# and the IDE imports it: the Load Density GUI, or a user in JEditor's +# in-process console. IDE_ONLY keeps it out of the processes the IDE starts (a +# load test needs the patching to run its users at once); a value the user set +# is kept, for both. +environ["LOCUST_SKIP_MONKEY_PATCH"] = environ.get("LOCUST_SKIP_MONKEY_PATCH") or IDE_ONLY from PySide6.QtCore import QTimer, QCoreApplication from PySide6.QtGui import QIcon diff --git a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py index 03eefe43..fd3b9007 100644 --- a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py +++ b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py @@ -12,7 +12,7 @@ from pybreeze.extend.process_executor.python_task_process_manager import default_interpreter from pybreeze.utils.logging.logger import pybreeze_logger -from pybreeze.utils.subprocess_util import no_window_creationflags +from pybreeze.utils.subprocess_util import child_environment, no_window_creationflags JUPYTER_STARTUP_TIMEOUT = 60 # How much of a failure's reason the tab shows: pip's stderr can run long @@ -159,7 +159,7 @@ def _start_server(self, python_exe: str, port: int) -> subprocess.Popen: "--ServerApp.password=", "--ServerApp.disable_check_xsrf=True", ], stdout=self._output, stderr=subprocess.STDOUT, text=True, - creationflags=no_window_creationflags()) + env=child_environment(), creationflags=no_window_creationflags()) @staticmethod def _port_open(port: int) -> bool: diff --git a/pybreeze/utils/subprocess_util.py b/pybreeze/utils/subprocess_util.py index 5ebfe54f..e2163571 100644 --- a/pybreeze/utils/subprocess_util.py +++ b/pybreeze/utils/subprocess_util.py @@ -11,16 +11,31 @@ # How long ending a process tree may take on the UI thread _TREE_KILL_SECONDS = 10 +# The value of an environment variable the IDE sets for its own process only; +# child_environment() leaves every variable with this value out +IDE_ONLY = "pybreeze-ide-only" + + +def child_environment() -> dict[str, str]: + """Return a copy of ``os.environ`` for a process the IDE starts. + + A variable the IDE set for itself alone (its value is ``IDE_ONLY``) is left + out: the IDE keeps locust from patching it with gevent + (``LOCUST_SKIP_MONKEY_PATCH``), and a load test it starts needs that + patching to run its users at once. + """ + return {name: value for name, value in os.environ.items() if value != IDE_ONLY} + def utf8_subprocess_env(encoding: str = "utf-8") -> dict[str, str]: - """Return a copy of ``os.environ`` forcing a child Python's stdio *encoding*. + """Return the environment for a child (``child_environment()``) forcing a child Python's stdio *encoding*. On Windows a child's piped stdout defaults to the console code page (e.g. cp950 / cp1252), so non-ASCII output would be mis-decoded by the utf-8 reader in the process managers and show up garbled. Pinning ``PYTHONIOENCODING`` makes the child emit the same encoding the manager decodes with. """ - env = os.environ.copy() + env = child_environment() env["PYTHONIOENCODING"] = encoding return env diff --git a/test/test_utils/test_jupyter_lifecycle.py b/test/test_utils/test_jupyter_lifecycle.py index 2234720e..633a3037 100644 --- a/test/test_utils/test_jupyter_lifecycle.py +++ b/test/test_utils/test_jupyter_lifecycle.py @@ -75,6 +75,19 @@ def test_it_is_bound_to_this_machine_and_takes_no_other_origin(self, app, launch assert not [arg for arg in argv if arg.startswith("--ServerApp.allow_origin")] thread.stop() + def test_it_runs_without_what_the_ide_set_for_itself(self, app, launched, monkeypatch): + # The IDE keeps locust from patching it with gevent; a notebook's kernel + # running a load test needs the patching + from pybreeze.utils.subprocess_util import IDE_ONLY + + monkeypatch.setenv("LOCUST_SKIP_MONKEY_PATCH", IDE_ONLY) + thread = jupyter_lab_thread.JupyterLauncherThread() + + thread.run() + + assert "LOCUST_SKIP_MONKEY_PATCH" not in launched[0].options["env"] + thread.stop() + def test_its_output_goes_to_a_file_not_a_pipe_nobody_reads(self, app, launched): import subprocess diff --git a/test/test_utils/test_startup_imports.py b/test/test_utils/test_startup_imports.py index 80436daf..d4e9bc8b 100644 --- a/test/test_utils/test_startup_imports.py +++ b/test/test_utils/test_startup_imports.py @@ -14,6 +14,8 @@ _WHAT_THE_START_LOADED = f"UNUSED_YET = {_UNUSED_YET!r}\n" + """ import ctypes +import os +from pybreeze.utils.subprocess_util import utf8_subprocess_env awareness = None if sys.platform == "win32": user32 = ctypes.windll.user32 @@ -23,13 +25,21 @@ result = { "awareness": awareness, "loaded": sorted(name for name in UNUSED_YET if name in sys.modules), + "locust_patching": { + "ide": os.environ.get("LOCUST_SKIP_MONKEY_PATCH"), + "child": utf8_subprocess_env().get("LOCUST_SKIP_MONKEY_PATCH"), + }, } """ @pytest.fixture(scope="module") def started(tmp_path_factory) -> dict: - return run_started_window(tmp_path_factory.mktemp("started"), _WHAT_THE_START_LOADED) + with pytest.MonkeyPatch.context() as patch: + # As a user starts it: an earlier test here may have imported the IDE, + # which sets this in this process + patch.delenv("LOCUST_SKIP_MONKEY_PATCH", raising=False) + return run_started_window(tmp_path_factory.mktemp("started"), _WHAT_THE_START_LOADED) @pytest.mark.skipif(sys.platform != "win32", reason="DPI awareness is a Windows process setting") @@ -45,3 +55,12 @@ def test_the_dpi_awareness_is_left_to_qt(started): def test_the_automation_packages_and_ssh_are_loaded_when_used(started): assert started["loaded"] == [] + + +def test_locust_leaves_the_ide_unpatched_and_the_processes_it_starts_patched(started): + # locust patches a process with gevent as it imports unless this is set. The + # IDE sets it for itself (the Load Density GUI imports locust there), and it + # went on to every process the IDE started: a load test of HttpUser users + # then ran them one at a time, and a 3 s test took over three minutes. + assert started["locust_patching"]["ide"] + assert started["locust_patching"]["child"] is None diff --git a/test/test_utils/test_subprocess_util.py b/test/test_utils/test_subprocess_util.py index 21ec07cc..9108f693 100644 --- a/test/test_utils/test_subprocess_util.py +++ b/test/test_utils/test_subprocess_util.py @@ -5,7 +5,9 @@ import pytest -from pybreeze.utils.subprocess_util import no_window_creationflags, utf8_subprocess_env +from pybreeze.utils.subprocess_util import ( + IDE_ONLY, child_environment, no_window_creationflags, utf8_subprocess_env, +) class TestUtf8SubprocessEnv: @@ -30,6 +32,35 @@ def test_child_actually_uses_the_encoding(self): assert result.stdout.strip().replace("-", "").lower() == "utf8" +class TestWhatTheIdeSetsForItself: + """A variable the IDE sets for its own process only stays out of the processes it starts.""" + + @staticmethod + def _set_for_the_ide(monkeypatch, name: str) -> None: + monkeypatch.setenv(name, IDE_ONLY) # restored when the test ends + + def test_a_child_does_not_get_it(self, monkeypatch): + self._set_for_the_ide(monkeypatch, "PYBREEZE_TEST_IDE_ONLY") + + assert "PYBREEZE_TEST_IDE_ONLY" not in child_environment() + assert "PYBREEZE_TEST_IDE_ONLY" not in utf8_subprocess_env() + + def test_one_the_user_set_is_passed_on(self, monkeypatch): + monkeypatch.setenv("PYBREEZE_TEST_IDE_ONLY", "1") + + assert child_environment()["PYBREEZE_TEST_IDE_ONLY"] == "1" + + def test_a_real_child_runs_without_it(self, monkeypatch): + self._set_for_the_ide(monkeypatch, "PYBREEZE_TEST_IDE_ONLY") + + result = subprocess.run( + [sys.executable, "-c", "import os; print(os.environ.get('PYBREEZE_TEST_IDE_ONLY'))"], + capture_output=True, text=True, env=utf8_subprocess_env(), timeout=60, check=True, + ) + + assert result.stdout.strip() == "None" + + class TestNoWindowCreationflags: def test_returns_int(self): assert isinstance(no_window_creationflags(), int) From 882b4032d8b98901410c1a7d4b27d8125a1b5518 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 16:10:39 +0800 Subject: [PATCH 156/166] Test each entry of the project tree's right-click menu --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- .../test_utils/test_file_tree_context_menu.py | 57 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index adae2fd5..db31ce14 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2693 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2701 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 0de6f6c6..26e2a3b0 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -868,3 +868,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/utils/subprocess_util.py`, `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py`, the three test files, `CLAUDE.md` (Conventions), `architecture.md` (§4), `architecture_explore.md` (main window, `subprocess_util.py`, test count), `progress.md` (#120) - **Checks**: from a process that had imported the IDE, the same HttpUser run started with `utf8_subprocess_env()` had gevent's patching and made 100 requests in 4.6 s. The subprocess, startup, JupyterLab and prthinker-contract tests pass (44, 14 skipped: no prthinker here). `test/test_utils` collects 2693 tests in 148 files. - **Open items**: `progress.md` #120. + +## U-20260925-104 · 2026-09-25 · Each entry of the project tree's right-click menu is tested · #test #file-tree + +- **What**: the coverage run of U-20260925-100 left `file_tree_context_menu.py` at 87 %, the lowest of the larger files; untested was the part of `_show_context_menu` that turns the entry chosen into its action, which blocks in `QMenu.exec()`. +- **Change**: `TestTheMenuEntries` in `test_file_tree_context_menu.py` opens the menu with a `QMenu` whose `exec()` picks the entry by its text, over an item and over empty space: New File, New Folder, Rename, Delete, Copy Path, Copy Relative Path and Reveal each call their own action with the item (Rename and Delete with the window too; the two copies differ only in `relative`), and over empty space only the two New entries of the seven can be chosen and nothing runs. +- **Files**: `test/test_utils/test_file_tree_context_menu.py`, `architecture_explore.md` (test count) +- **Checks**: the eight tests pass as the code stands; the file's 74 tests pass. `test/test_utils` collects 2701 tests in 148 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 98d28cef..bc0b8f5d 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-104 | 2026-09-25 | Each entry of the project tree's right-click menu is tested | #test #file-tree | [2026-09-c](2026-09-c.md) | | U-20260925-103 | 2026-09-25 | Load tests the IDE starts are patched with gevent again | #fix #load-density #subprocess | [2026-09-c](2026-09-c.md) | | U-20260925-102 | 2026-09-25 | The IDE starts about 1.8 s sooner: the automation GUIs and SSH load when opened | #perf #startup | [2026-09-c](2026-09-c.md) | | U-20260925-101 | 2026-09-25 | The IDE is per-monitor DPI aware again: AutoControl is imported when used | #fix #startup #autocontrol | [2026-09-c](2026-09-c.md) | @@ -506,4 +507,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 103 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 104 | diff --git a/test/test_utils/test_file_tree_context_menu.py b/test/test_utils/test_file_tree_context_menu.py index def5da2d..b0f59173 100644 --- a/test/test_utils/test_file_tree_context_menu.py +++ b/test/test_utils/test_file_tree_context_menu.py @@ -696,3 +696,60 @@ def missing(_command): ctx._action_reveal_in_explorer(tree, tmp_path) assert len(warnings) == 1 and "xdg-open" in warnings[0] + + +class TestTheMenuEntries: + """Each entry of the right-click menu does its own action, and those that need an item wait for one.""" + + _HANDLERS = ("_action_new_file", "_action_new_folder", "_action_rename", "_action_delete", + "_action_copy_path", "_action_reveal_in_explorer") + + @staticmethod + def _open(tree, monkeypatch, *, under_the_cursor: Path | None, choose: str | None) -> tuple[list, dict]: + """Open the menu over *under_the_cursor* and pick the entry reading *choose*.""" + from PySide6.QtWidgets import QMenu + + calls: list = [] + enabled: dict[str, bool] = {} + + class ChoosingMenu(QMenu): + def exec(self, *args): + enabled.update({action.text(): action.isEnabled() for action in self.actions() if action.text()}) + return next((action for action in self.actions() if action.text() == choose), None) + + monkeypatch.setattr(ctx, "QMenu", ChoosingMenu) + monkeypatch.setattr(ctx, "_get_path_from_index", lambda _tree, _index: under_the_cursor) + for name in TestTheMenuEntries._HANDLERS: + monkeypatch.setattr(ctx, name, lambda *args, _name=name, **kwargs: calls.append((_name, args, kwargs))) + ctx._show_context_menu(QPoint(1, 1), tree, "the window") + return calls, enabled + + @pytest.mark.parametrize("key, handler, arguments, keywords", [ + ("file_tree_ctx_new_file", "_action_new_file", ("path",), {}), + ("file_tree_ctx_new_folder", "_action_new_folder", ("path",), {}), + ("file_tree_ctx_rename", "_action_rename", ("window", "path"), {}), + ("file_tree_ctx_delete", "_action_delete", ("window", "path"), {}), + ("file_tree_ctx_copy_path", "_action_copy_path", ("path",), {"relative": False}), + ("file_tree_ctx_copy_relative_path", "_action_copy_path", ("path",), {"relative": True}), + ("file_tree_ctx_reveal_in_explorer", "_action_reveal_in_explorer", ("path",), {}), + ]) + def test_an_entry_does_its_own_action(self, tree, tmp_path, monkeypatch, key, handler, arguments, keywords): + from je_editor import language_wrapper + + item = tmp_path / "a.py" + calls, _enabled = self._open(tree, monkeypatch, under_the_cursor=item, + choose=language_wrapper.language_word_dict.get(key)) + + given = {"path": item, "window": "the window"} + assert calls == [(handler, (tree, *(given[name] for name in arguments)), keywords)] + + def test_on_empty_space_only_new_entries_can_be_chosen(self, tree, monkeypatch): + from je_editor import language_wrapper + + words = language_wrapper.language_word_dict + calls, enabled = self._open(tree, monkeypatch, under_the_cursor=None, choose=None) + + assert calls == [] + assert {text for text, on in enabled.items() if on} == { + words.get("file_tree_ctx_new_file"), words.get("file_tree_ctx_new_folder")} + assert len(enabled) == 7 From 06a65a597f2a408f0016ab4043ff42b43dafc42b Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 17:22:40 +0800 Subject: [PATCH 157/166] Ship the window icon with the package so the IDE shows it wherever it starts --- architecture_explore.md | 4 +-- dev.toml | 4 +++ docs/updates/2026-09-c.md | 9 ++++++ docs/updates/README.md | 3 +- exe/auto_py_to_exe_setting.json | 6 +++- progress.md | 2 +- pybreeze/pybreeze_ui/editor_main/main_ui.py | 6 +++- .../editor_main}/pybreeze_icon.ico | Bin pyproject.toml | 4 +++ test/test_utils/test_window_icon.py | 30 ++++++++++++++++++ 10 files changed, 62 insertions(+), 6 deletions(-) rename {exe => pybreeze/pybreeze_ui/editor_main}/pybreeze_icon.ico (100%) create mode 100644 test/test_utils/test_window_icon.py diff --git a/architecture_explore.md b/architecture_explore.md index db31ce14..38474191 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -70,7 +70,7 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` - `show_only_warnings_in_code_result()`(`pybreeze_ui/code_result_logs.py`)— JEditor 剛把一個 `RedirectStdErr` 掛到當下每個 logger 上、收到的顯示在 Code Result;自動化套件 import 時把 root 設成 DEBUG,所以開檔就有 gitpython 的除錯訊息以紅字出現。把這個 handler 的門檻調到 WARNING,logger 本身的層級不動 - 刪掉 JEditor 原本的 Help 選單 - - 設定標題、Windows AppUserModelID、圖示 + - 設定標題、Windows AppUserModelID、圖示(`pybreeze_icon.ico`,在 `main_ui.py` 旁邊、以 package data 隨套件發佈:`pyproject.toml` / `dev.toml` 的 `[tool.setuptools.package-data]`,執行檔建置用 `datas` 帶進去,所以不論從哪個資料夾啟動都有圖示) - `add_menu_to_menubar()` — 建構全部選單(見 §5) - `syntax_extend_package()` — 註冊 `.json` / `.yml` / `.yaml` 自動化關鍵字高亮 - 依 `EDITOR_EXTEND_TAB` 註冊表加入外部擴充分頁(`_add_extend_tabs()`:每一個分頁各自建,建不起來的只記 log,不會讓整個 IDE 起不來) @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 148 個 `test_*.py`、2701 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 149 個 `test_*.py`、2703 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/dev.toml b/dev.toml index b6d349fb..6494cb0e 100644 --- a/dev.toml +++ b/dev.toml @@ -43,3 +43,7 @@ content-type = "text/markdown" [tool.setuptools.packages] find = { namespaces = false } + +# The main window's icon, loaded from beside main_ui.py +[tool.setuptools.package-data] +"pybreeze.pybreeze_ui.editor_main" = ["pybreeze_icon.ico"] diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 26e2a3b0..55d25484 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -876,3 +876,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_file_tree_context_menu.py`, `architecture_explore.md` (test count) - **Checks**: the eight tests pass as the code stands; the file's 74 tests pass. `test/test_utils` collects 2701 tests in 148 files. - **Open items**: none. + +## U-20260925-105 · 2026-09-25 · The main window shows PyBreeze's icon wherever the IDE starts from · #fix #packaging #ui + +- **What**: `PyBreezeMainWindow` read its icon from `pybreeze_icon.ico` in the working folder, and the only copy was `exe/pybreeze_icon.ico`, which is not part of the package: `python -m pybreeze`, an installed package, and the built executable (its own folder has no copy; the build only embedded it in the `.exe` file) all showed the window without it. +- **Change**: the icon moves to `pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico` and `main_ui.py` loads it from beside itself (`_ICON_PATH`). `pyproject.toml` and `dev.toml` list it as package data (`[tool.setuptools.package-data]`); `exe/auto_py_to_exe_setting.json` takes the executable's icon from there and bundles it for the window (`datas`, to the same place under the package). `progress.md` #112 names the new path. +- **Tests**: `test_window_icon.py` (new): the real main window, started in an empty temporary folder, has an icon; the icon is a file beside `main_ui.py` with an `.ico` header. Both failed before the change. +- **Files**: `pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico` (moved from `exe/`), `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `pyproject.toml`, `dev.toml`, `exe/auto_py_to_exe_setting.json`, `test/test_utils/test_window_icon.py` (new), `architecture_explore.md` (§3 start, test count), `progress.md` (#112) +- **Checks**: `python -m build` (setuptools from `pyproject.toml`) makes a wheel with `pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico` in it, and an sdist with it too. The whole executable was not rebuilt here; a PyInstaller 6 build (Python 3.14) of a stand-in script importing `pybreeze.pybreeze_ui.editor_main`, with the same `datas` entry, found the icon beside the frozen module (`_internal/pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico`), where `_ICON_PATH` looks. `test/test_utils` collects 2703 tests in 149 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index bc0b8f5d..3c052401 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-105 | 2026-09-25 | The main window shows PyBreeze's icon wherever the IDE starts from | #fix #packaging #ui | [2026-09-c](2026-09-c.md) | | U-20260925-104 | 2026-09-25 | Each entry of the project tree's right-click menu is tested | #test #file-tree | [2026-09-c](2026-09-c.md) | | U-20260925-103 | 2026-09-25 | Load tests the IDE starts are patched with gevent again | #fix #load-density #subprocess | [2026-09-c](2026-09-c.md) | | U-20260925-102 | 2026-09-25 | The IDE starts about 1.8 s sooner: the automation GUIs and SSH load when opened | #perf #startup | [2026-09-c](2026-09-c.md) | @@ -507,4 +508,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 104 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 105 | diff --git a/exe/auto_py_to_exe_setting.json b/exe/auto_py_to_exe_setting.json index a320f5d8..5b9b1592 100644 --- a/exe/auto_py_to_exe_setting.json +++ b/exe/auto_py_to_exe_setting.json @@ -19,7 +19,11 @@ }, { "optionDest": "icon_file", - "value": "D:\\Codes/PyBreeze/exe/pybreeze_icon.ico" + "value": "D:\\Codes/PyBreeze/pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico" + }, + { + "optionDest": "datas", + "value": "D:/Codes/PyBreeze/pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico;pybreeze/pybreeze_ui/editor_main" }, { "optionDest": "name", diff --git a/progress.md b/progress.md index 69118151..3e645482 100644 --- a/progress.md +++ b/progress.md @@ -20,7 +20,7 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#109** [DECIDE] `je-editor>=1.0.27` (`requirements.txt`, `pyproject.toml`, `dev.toml`) lets an environment keep a gitpython below 3.1.59, which has CVE-2026-78676 (a config write turns a quoted value into a live `core.hooksPath`: code runs on the next hook) and CVE-2026-78679; the IDE's git features use it (JEditor's `je_editor/git_client/`). je-editor 1.0.28 differs from 1.0.27 only in requiring `gitpython>=3.1.59`, and 3.1.59 still has CVE-2026-87818 (the diff API's `--no-index` reads any path), fixed in 3.1.60. Should the dependencies say `je-editor>=1.0.28`, declare `gitpython>=3.1.60`, or both? JEditor's own floor is its to raise (`D:\Codes\JEDITOR\pyproject.toml:18`). - **#110** [BLOCKED] A file opened from the project tree shows as unsaved (" *" on its tab, `_is_modified` true) before anything is typed: JEditor's `EditorWidget.open_an_file` fills the editor with `setPlainText`, which fires `_on_text_changed`, and never clears the mark afterwards (`je_editor/pyside_ui/main_ui/editor/editor_widget.py:295` and `:347-358` in `D:\Codes\JEDITOR` at 1bf81e3; plain `EditorMain` does the same). Needs `open_an_file` to clear the mark (`mark_saved()`) once the file is loaded. - **#111** [DECIDE] AI Code Review, CoT Code Review and Skill Send refuse an endpoint on this machine or on a private network (`validate_url` in `ai_code_review_gui.py:77`, `code_review_thread.py:33`, `skills_send_gui.py:55`; `url_validation._is_blocked_ip`), so a local model server (Ollama, LM Studio, a prthinker server on localhost) cannot be used from them, although CoT Code Review and Skill Send suggested `http://127.0.0.1:5000/api` until U-20260925-11. The Security › Network rule rejects loopback and private addresses for every user-supplied URL. Should these panels let the user allow a loopback (or private) endpoint, for example after a confirmation naming the address, or stay public-only? -- **#112** [DECIDE] Two of the executable build configs still build the IDE under its old name: `exe/auto_py_to_exe_setting_linux.json` (name AutomationIDE, script `exe/start_automation_editor.py` and icon `exe/je_driver_icon.ico`, neither of which exists any more, all under `C:/CodeWorkspace/Python/AutomationIDE`) and the InstallForge project `exe/automation_ide_setup_config.ifp` (program AutomationIDE 1.0.9, the AutomationIDE repository as its website and licence link, the same icon, `...\AutomationIDE\output\AutomationIDE.exe`). The Windows `exe/auto_py_to_exe_setting.json` builds PyBreeze from `exe/start_pybreeze.py` with `exe/pybreeze_icon.ico` (987252e). Should the two be brought over to PyBreeze the same way (their paths are a build machine's), or deleted if they are no longer used? +- **#112** [DECIDE] Two of the executable build configs still build the IDE under its old name: `exe/auto_py_to_exe_setting_linux.json` (name AutomationIDE, script `exe/start_automation_editor.py` and icon `exe/je_driver_icon.ico`, neither of which exists any more, all under `C:/CodeWorkspace/Python/AutomationIDE`) and the InstallForge project `exe/automation_ide_setup_config.ifp` (program AutomationIDE 1.0.9, the AutomationIDE repository as its website and licence link, the same icon, `...\AutomationIDE\output\AutomationIDE.exe`). The Windows `exe/auto_py_to_exe_setting.json` builds PyBreeze from `exe/start_pybreeze.py` with the icon `pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico`, also bundled as data for the window. Should the two be brought over to PyBreeze the same way (their paths are a build machine's), or deleted if they are no longer used? - **#113** [BLOCKED] A translation plugin's language is listed in the Language menu under its key, not the name it registers: `register_natural_language(key, display_name, word_dict)` (`je_editor/plugins/__init__.py`) writes the dictionary into `language_wrapper.choose_language_dict` but not `display_names`, so `available_languages()` lists it and `display_name()` gives the key (`je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py:47-49`), and the loop over `get_all_natural_languages()` below skips it as already listed. Seen with the Sphinx Plugins page's French example ("French" shown, not "Français") on je_editor 1.0.27. Needs JEditor's `register_natural_language` to call `language_wrapper.register_language(key, word_dict, display_name)`. - **#114** [BLOCKED] File > Save File, Run Program and Run Debugger open a Save As dialog every time, starting in the working folder with no name filled in, even for a tab that already has a file: each calls `choose_file_get_save_file_path()` (`je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py:96`, `.../run_menu/under_run_menu/build_program_menu.py:95`, `build_debug_menu.py:106`), which always asks, on je_editor 1.0.27. PyBreeze's own Run with... saves in place (`save_current_file_for_run`). Needs JEditor to write a tab with a file where it is, and ask only for one without; the Sphinx File/Run/Text page says what it does now. - **#115** [BLOCKED] Run Debugger works once per editor tab: when the `pdb` run ends, `ExecManager.full_exit_program()` clears the tab's `exec_program`, not `exec_python_debugger` (`je_editor/pyside_ui/code/code_process/code_exec.py:228`), and nothing else clears it, not Stop current program (`build_run_menu.py:117`) or Stop All, so the next Run Debugger in that tab says a program is still running (`build_debug_menu.py:102`). Reproduced on je_editor 1.0.27 in the real window: after `pdb` quit (exit 0) the slot was still set and a second Run Debugger was refused. Needs the debugger's manager to clear the slot it was stored in. diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index 544eccc7..895cd5f3 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -40,6 +40,10 @@ # The theme until one is picked from UI Style, as JEditor has it DEFAULT_THEME = "dark_amber.xml" +# Shipped beside this module (package data): it was read from the working +# folder, which a started IDE never has, and the window had no icon +_ICON_PATH = Path(__file__).with_name("pybreeze_icon.ico") + def _close_guarded(widget: QWidget, *steps) -> None: """Run *widget*'s closing *steps*, logging a failure instead of raising it. @@ -92,7 +96,7 @@ def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, # Icon if not extend: - self.icon_path = Path(os.getcwd()) / "pybreeze_icon.ico" + self.icon_path = _ICON_PATH self.icon = QIcon(str(self.icon_path)) if not self.icon.isNull(): self.setWindowIcon(self.icon) diff --git a/exe/pybreeze_icon.ico b/pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico similarity index 100% rename from exe/pybreeze_icon.ico rename to pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico diff --git a/pyproject.toml b/pyproject.toml index d01a1eef..3facb552 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,10 @@ content-type = "text/markdown" [tool.setuptools.packages] find = { namespaces = false } +# The main window's icon, loaded from beside main_ui.py +[tool.setuptools.package-data] +"pybreeze.pybreeze_ui.editor_main" = ["pybreeze_icon.ico"] + [tool.bandit] # Codacy / SonarCloud run Bandit across the tree; tests use `assert` intentionally # (pytest relies on it), and subprocess is a required dependency of the IDE's diff --git a/test/test_utils/test_window_icon.py b/test/test_utils/test_window_icon.py new file mode 100644 index 00000000..be9db2a0 --- /dev/null +++ b/test/test_utils/test_window_icon.py @@ -0,0 +1,30 @@ +"""The main window shows PyBreeze's icon wherever the IDE is started from.""" +from __future__ import annotations + +from pathlib import Path + +from test_utils.started_window import run_started_window + +_ICON = """ +result = {"has_icon": not window.windowIcon().isNull(), + "sizes": [size.width() for size in window.windowIcon().availableSizes()]} +""" + + +def test_the_window_has_pybreezes_icon_whatever_the_working_folder(tmp_path): + # It was read from pybreeze_icon.ico in the working folder, which a started + # IDE (python -m pybreeze, an installed package) never has: the window had + # no icon. The child here runs in an empty temporary folder. + seen = run_started_window(tmp_path, _ICON) + + assert seen["has_icon"] + assert seen["sizes"] + + +def test_the_icon_is_package_data(): + # Shipped inside the package, beside the module that loads it + from pybreeze.pybreeze_ui.editor_main import main_ui + + icon = Path(main_ui.__file__).with_name("pybreeze_icon.ico") + assert icon.is_file() + assert icon.read_bytes()[:4] == b"\x00\x00\x01\x00" # an .ico file's header From 378c078bf379f17c73082787a8aa8c86fdee2d4e Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 17:22:51 +0800 Subject: [PATCH 158/166] Test the packaged build's regex worker timeout and errors --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++ docs/updates/README.md | 3 +- test/test_utils/test_regex_tester.py | 57 ++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index 38474191..f1b2d3e8 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 149 個 `test_*.py`、2703 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 149 個 `test_*.py`、2707 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 55d25484..39a34de2 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -885,3 +885,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico` (moved from `exe/`), `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `pyproject.toml`, `dev.toml`, `exe/auto_py_to_exe_setting.json`, `test/test_utils/test_window_icon.py` (new), `architecture_explore.md` (§3 start, test count), `progress.md` (#112) - **Checks**: `python -m build` (setuptools from `pyproject.toml`) makes a wheel with `pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico` in it, and an sdist with it too. The whole executable was not rebuilt here; a PyInstaller 6 build (Python 3.14) of a stand-in script importing `pybreeze.pybreeze_ui.editor_main`, with the same `datas` entry, found the icon beside the frozen module (`_internal/pybreeze/pybreeze_ui/editor_main/pybreeze_icon.ico`), where `_ICON_PATH` looks. `test/test_utils` collects 2703 tests in 149 files. - **Open items**: none. + +## U-20260925-106 · 2026-09-25 · The packaged build's regex worker: its timeout and its errors are tested · #test #regex + +- **What**: `regex_tester.py` was at 86 % in the coverage run of U-20260925-100. Untested was most of the path a packaged build takes (`_find_in_spawned_process`, a `spawn` child, since a frozen app has no interpreter to run the worker script with): a pattern stopped for running too long, a child that reports an error, and `_matches_into_pipe`, which runs in the child where coverage does not follow. +- **Change**: `test_regex_tester.py`: a catastrophic pattern in the spawned worker is stopped after its 3 s with the timeout message and leaves no worker registered; a pattern the child cannot compile comes back as a `RegexTesterException`; `_matches_into_pipe`, given a stand-in for its end of the pipe, sends the matches (the same as `find_matches`, `1` and `22`) or an error, and closes it either way. Checked on the way that the child's `find_matches` matches in its own process: it does not take the bounded path again, so a frozen child does not spawn one of its own. +- **Files**: `test/test_utils/test_regex_tester.py`, `architecture_explore.md` (test count) +- **Checks**: the four tests pass as the code stands; the file's 32 tests pass. `test/test_utils` collects 2707 tests in 149 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 3c052401..eac6eade 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-106 | 2026-09-25 | The packaged build's regex worker: its timeout and its errors are tested | #test #regex | [2026-09-c](2026-09-c.md) | | U-20260925-105 | 2026-09-25 | The main window shows PyBreeze's icon wherever the IDE starts from | #fix #packaging #ui | [2026-09-c](2026-09-c.md) | | U-20260925-104 | 2026-09-25 | Each entry of the project tree's right-click menu is tested | #test #file-tree | [2026-09-c](2026-09-c.md) | | U-20260925-103 | 2026-09-25 | Load tests the IDE starts are patched with gevent again | #fix #load-density #subprocess | [2026-09-c](2026-09-c.md) | @@ -508,4 +509,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 105 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 106 | diff --git a/test/test_utils/test_regex_tester.py b/test/test_utils/test_regex_tester.py index e5af7424..89ef704a 100644 --- a/test/test_utils/test_regex_tester.py +++ b/test/test_utils/test_regex_tester.py @@ -206,3 +206,60 @@ def test_the_packaged_builds_spawned_worker_finds_matches(self): assert found == regex_tester.find_matches(r"\d+", "a1 b22") assert not regex_tester._RUNNING + + def test_the_packaged_builds_worker_is_stopped_when_it_runs_too_long(self): + import time + + from pybreeze.utils.exception.exceptions import RegexTesterException + from pybreeze.utils.regex_tools import regex_tester + + started = time.monotonic() + with pytest.raises(RegexTesterException, match="still running"): + regex_tester._find_in_spawned_process("(a+)+$", "a" * 40 + "b", [], 3.0) + + assert time.monotonic() - started < 20 + assert not regex_tester._RUNNING + + def test_the_packaged_builds_worker_reports_a_pattern_it_cannot_run(self): + from pybreeze.utils.exception.exceptions import RegexTesterException + from pybreeze.utils.regex_tools import regex_tester + + with pytest.raises(RegexTesterException): + regex_tester._find_in_spawned_process("(", "abc", [], 30) + + assert not regex_tester._RUNNING + + +class TestWhatTheSpawnedWorkerSends: + """_matches_into_pipe, run here with a stand-in for its end of the pipe.""" + + class _Pipe: + def __init__(self) -> None: + self.sent: list = [] + self.closed = False + + def send(self, message) -> None: + self.sent.append(message) + + def close(self) -> None: + self.closed = True + + def test_the_matches(self): + from pybreeze.utils.regex_tools.regex_tester import _matches_into_pipe + + pipe = self._Pipe() + _matches_into_pipe(pipe, r"\d+", "a1 b22", []) + + assert pipe.sent == [("matches", find_matches(r"\d+", "a1 b22"))] + assert [match.matched_text for match in pipe.sent[0][1]] == ["1", "22"] + assert pipe.closed + + def test_the_error_for_a_pattern_that_does_not_compile(self): + from pybreeze.utils.regex_tools.regex_tester import _matches_into_pipe + + pipe = self._Pipe() + _matches_into_pipe(pipe, "(", "abc", []) + + ((kind, message),) = pipe.sent + assert kind == "error" and message + assert pipe.closed From e7cc100515b6891c2072bb35b4bf410a60da5fae Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 17:49:20 +0800 Subject: [PATCH 159/166] Apply the theme once as the main window opens --- architecture.md | 9 +++- architecture_explore.md | 10 ++--- docs/updates/2026-09-c.md | 9 ++++ docs/updates/README.md | 3 +- pybreeze/pybreeze_ui/editor_main/main_ui.py | 30 ++++++++----- test/test_utils/test_jeditor_contract.py | 6 +++ test/test_utils/test_start_theme.py | 48 +++++++++++++++++++++ 7 files changed, 97 insertions(+), 18 deletions(-) diff --git a/architecture.md b/architecture.md index aeb1f090..25b0afa5 100644 --- a/architecture.md +++ b/architecture.md @@ -78,7 +78,10 @@ python -m pybreeze → start_editor() → QApplication → open_main_window() → EditorMain.__init__(extend=True) [JEditor builds the editor, loads jeditor_plugins/] → drop JEditor Help menu → add_menu_to_menubar() → syntax_extend_package() → EDITOR_EXTEND_TAB tabs → setup_file_tree_context_menu() - → apply_stylesheet(theme) → showMaximized() → startup_setting() → exec() → os._exit() + [EditorMain.__init__ has applied the saved settings and UI Style theme: startup_setting()] + → a theme given to start_editor(): saved as ui_style, startup_setting() again + (none given: only the window's own style sheet is set again) + → showMaximized() → exec() → os._exit() ``` Before PySide6 is imported, `main_ui.py` sets `LOCUST_SKIP_MONKEY_PATCH` (to `IDE_ONLY`, unless the user @@ -160,7 +163,9 @@ Run with… / Plugins menu (menu/plugin_menu/) → get_all_plugin_run_configs() back after `rename_self_tab()` clears it), on `EditorMain.close_tab(index)` (overridden to ask a tab's `may_close()` first, and to delete a closed tool tab, which its `removeTab` keeps) and on `CodeEditor`'s `reset_highlighter()`, `load_git_baseline()` and `start_language_server()` (a rename moves the tab the way `open_an_file` does), on `EditorMain`'s - `run_menu.stop_all_program_action` (Stop All Program also stops every run window's run), its + `run_menu.stop_all_program_action` (Stop All Program also stops every run window's run), on + `EditorMain.__init__` calling `startup_setting()` (the window is built with the saved settings and + theme; `open_main_window()` applies them again only for a theme given to `start_editor()`), its `dock_menu` and its AI submenu `dock_ai_menu` (PyBreeze's AI docks join it; without one they get an AI submenu of their own, `menu/tools/tools_menu.py`), on the syntax highlighter taking a theme colour key (`warning_output_color`, `diff_modified_marker_color`, in both JEditor's dark and light diff --git a/architecture_explore.md b/architecture_explore.md index f1b2d3e8..4f00257e 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -62,12 +62,12 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE ## 3. 啟動流程 -`pybreeze/pybreeze_ui/editor_main/main_ui.py:191` 的 `start_editor()`(第 2 步到 `startup_setting()` 在 `open_main_window()`,它回傳視窗給 `start_editor()` 握到程式結束): +`pybreeze/pybreeze_ui/editor_main/main_ui.py:216` 的 `start_editor()`(第 2 到 4 步在 `open_main_window()`,它回傳視窗給 `start_editor()` 握到程式結束): 1. 取得(或建立)`QApplication`,裝上 `collect_garbage_on_gui_thread()`(`pybreeze_ui/gui_thread_gc.py`:關掉自動垃圾回收,改在 UI 執行緒上定時回收,見 §16) 2. 建立 `PyBreezeMainWindow`,其 `__init__` 依序: - `update_language_dict()` 併入 PyBreeze 的 760 條翻譯——**必須在 `super().__init__` 之前**:JEditor 在那裡依設定挑啟動語言,英文以外的語言讀的是當下合併出來的一份副本,之後才加進去的字串它看不到,選單拿到 `None` 標題就讓 Qt 當掉(access violation) - - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/` + - `super().__init__(..., extend=True)` — JEditor 在此已呼叫 `load_external_plugins()`,自動掃描 CWD 下的 `jeditor_plugins/`,也以 `startup_setting()` 套上存下的設定與 UI Style 主題 - `show_only_warnings_in_code_result()`(`pybreeze_ui/code_result_logs.py`)— JEditor 剛把一個 `RedirectStdErr` 掛到當下每個 logger 上、收到的顯示在 Code Result;自動化套件 import 時把 root 設成 DEBUG,所以開檔就有 gitpython 的除錯訊息以紅字出現。把這個 handler 的門檻調到 WARNING,logger 本身的層級不動 - 刪掉 JEditor 原本的 Help 選單 - 設定標題、Windows AppUserModelID、圖示(`pybreeze_icon.ico`,在 `main_ui.py` 旁邊、以 package data 隨套件發佈:`pyproject.toml` / `dev.toml` 的 `[tool.setuptools.package-data]`,執行檔建置用 `datas` 帶進去,所以不論從哪個資料夾啟動都有圖示) @@ -77,8 +77,8 @@ PyBreeze 是一個「自動化優先」的 Python IDE,建構在 **PySide6 + JE - `setup_file_tree_context_menu()` — 掛上檔案樹右鍵選單,以及焦點在樹上時的 F2(重新命名)與 Delete(刪除,先問、預設否)快捷鍵(`_attach_keys()`,`WidgetShortcut`,走選單的同一組動作)。改名時開著的分頁跟著檔案走(改資料夾也一樣,底下每個開著的檔案都跟著走):先停掉分頁的自動存檔、改名、再用新路徑重開一條(`_stop_auto_save()` / `_start_auto_save()`)——JEditor 的存檔執行緒只認開檔當下的路徑,沒辦法改指向。外部修改監視也跟著搬(改名前就先移除,檔案搬走後 Windows 放不掉舊名),並照 `open_an_file` 重載語法高亮、git 基準與語言伺服器(`rename_self_tab()` 會清掉「未儲存」標記,有未存的編輯就用 `_on_text_changed()` 放回去,否則改名後五秒內關分頁會直接丟掉編輯);Dock Editor(`FullEditorWidget`,關閉時才寫回、檔案不存在就不寫)的 `current_file` 也改指新路徑(`_dock_editors_under()`)。新增與改名的名稱不能帶磁碟代號、根目錄、`..` 或 `:`,也不能解析到資料夾外(`_inside()`;改名只能是單一名稱)。刪除先移到系統的回收筒(`_move_to_trash()`:`QFile.moveToTrash`,Windows 的資源回收筒、macOS 與 freedesktop 的垃圾桶);沒有回收筒可用時再問一次(預設否)才永久刪除,資料夾用 `remove_folder()`(唯讀檔清掉唯讀屬性再刪,git 的物件檔就是唯讀);符號連結與 junction 只刪連結本身,不進回收筒。刪除時同樣用 `_editors_under()`:檔案或資料夾底下每個開著的分頁先停掉自動存檔,再刪;刪完只關掉檔案真的不見了的分頁,刪不掉(被鎖住、唯讀)的檔案分頁留著、自動存檔重開。「在檔案總管中顯示」由 `reveal_command()` 組指令:Windows 用 Explorer 的 `/select,`、macOS 用 `open -R` 把檔案選起來,其他平台 `xdg-open` 只能開資料夾;啟動失敗(例如沒有 `xdg-open`)經 `_perform_file_op()` 跳警告 - `close_tab()` 覆寫 JEditor 的:分頁有 `may_close()` 就先問(提示詞編輯器、架構圖編輯器有未存的變更時會問);關掉的工具分頁 `deleteLater()`(JEditor 的 `removeTab` 不刪 widget,關過的工具分頁會留到 IDE 結束),JEditor 自己的編輯器分頁不動,關閉 IDE 時也先問過每個分頁與 dock,有一個說不就取消關閉 - `debug_mode=True` 時啟動 10 秒自動關閉 `QTimer`(CI 用) -3. `apply_stylesheet()` 套 qt_material 主題:JEditor 存下的 `ui_style`(UI Style 選單選的,沒選過是 `dark_amber.xml`)。`start_editor(theme=...)` 給的主題先寫進 `user_setting_dict["ui_style"]`:`startup_setting()` 會把存下的主題蓋在之前套的任何主題上,以前這個參數因此從來看不到 -4. `showMaximized()` → `startup_setting()` → `app.exec()` +3. `start_editor(theme=...)` 給了主題時(`_apply_given_theme()`):寫進 `user_setting_dict["ui_style"]` 成為選定的主題,再跑一次 `startup_setting()` 套上;它失敗時記 log,仍用 qt_material 的 `apply_stylesheet()` 套上這個主題。沒給主題就不再套:建構子已經套過 JEditor 存下的 `ui_style`(UI Style 選的,沒選過是 `dark_amber.xml`),而套一次主題要將近一秒;只把視窗自己的字型 style sheet 再設一次(`startup_setting()` 先設它、後套主題,不再設一次的話工具列比給了主題時高 4 px) +4. `showMaximized()` → `app.exec()` 5. 離開時以 `os._exit(ret)` 硬退出(避開 Qt 拆解殘留執行緒) 模組層級有一個副作用:`main_ui.py` 在匯入 PySide6 之前就設定 `LOCUST_SKIP_MONKEY_PATCH`(值是 `subprocess_util.IDE_ONLY`;使用者自己設過就沿用),避免 locust 一 import 就對整個行程做的 gevent monkey patch 破壞 Qt(Load Density GUI、JEditor 行程內的 IPython console 都可能 import 它)。IDE 啟動的行程拿到的是 `child_environment()`,不帶這個值:負載測試要靠 patch 才能讓使用者同時跑,帶著它時 HttpUser 一個接一個跑,3 秒的測試跑了三分多鐘。 @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 149 個 `test_*.py`、2707 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 149 個 `test_*.py`、2711 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 39a34de2..0db127d2 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -893,3 +893,12 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_regex_tester.py`, `architecture_explore.md` (test count) - **Checks**: the four tests pass as the code stands; the file's 32 tests pass. `test/test_utils` collects 2707 tests in 149 files. - **Open items**: none. + +## U-20260925-107 · 2026-09-25 · Opening the main window takes about 1 s instead of 3: the theme is applied once · #perf #startup + +- **What**: `open_main_window()` applied the theme to the application three times. JEditor's constructor already runs `startup_setting()` (the saved settings, the UI Style theme among them); after it came qt_material's `apply_stylesheet()` with the same theme (0.72–0.77 s) and, once the window was shown, `startup_setting()` again (0.92–1.24 s), whose file and session restores JEditor guards against running twice, leaving only the theme and fonts done again. Counted in a child: `QApplication.setStyleSheet` ran three times with or without a theme given at launch. +- **Change**: `open_main_window()` builds the window and shows it. Only a theme given to `start_editor()` is applied again (`_apply_given_theme()`: saved as `ui_style`, then `startup_setting()`; if that fails, the error is logged and qt_material's `apply_stylesheet()` still applies the theme). Otherwise the window's own font style sheet is set once more (0.08 s): `startup_setting()` sets it before the theme, and without setting it again the toolbar was 4 px taller (59 px, not 55) than the IDE has always shown it. `DEFAULT_THEME` went with the call that used it. +- **Tests**: `test_start_theme.py`: the theme is applied once when saved, twice when given at launch (it failed before: three times each), and the toolbar is as tall with `dark_amber.xml` saved as with it given (it failed without the style sheet set again: 59 against 55). The existing tests that the given, saved and default themes are the ones shown pass. `test_jeditor_contract.py` pins that `EditorMain.__init__` calls `startup_setting()`, which `architecture.md` §6 now lists among what PyBreeze relies on. +- **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_start_theme.py`, `test/test_utils/test_jeditor_contract.py`, `architecture.md` (§4 flow, §6), `architecture_explore.md` (§3 start, test count) +- **Checks**: `open_main_window()` before (378c078) and after, alternated five times each: median 3.32 s → 1.07 s. Images of the started window (offscreen, 1772 × 796) with `light_blue.xml` saved and with nothing saved are identical pixel for pixel before and after. The start and extend startup tests exit 0. `test/test_utils` collects 2711 tests in 149 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index eac6eade..7f343d78 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-107 | 2026-09-25 | Opening the main window takes about 1 s instead of 3: the theme is applied once | #perf #startup | [2026-09-c](2026-09-c.md) | | U-20260925-106 | 2026-09-25 | The packaged build's regex worker: its timeout and its errors are tested | #test #regex | [2026-09-c](2026-09-c.md) | | U-20260925-105 | 2026-09-25 | The main window shows PyBreeze's icon wherever the IDE starts from | #fix #packaging #ui | [2026-09-c](2026-09-c.md) | | U-20260925-104 | 2026-09-25 | Each entry of the project tree's right-click menu is tested | #test #file-tree | [2026-09-c](2026-09-c.md) | @@ -509,4 +510,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 106 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 107 | diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index 895cd5f3..f73bdd15 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -37,9 +37,6 @@ EDITOR_EXTEND_TAB: dict[str, type[QWidget]] = { } -# The theme until one is picked from UI Style, as JEditor has it -DEFAULT_THEME = "dark_amber.xml" - # Shipped beside this module (package data): it was read from the working # folder, which a started IDE never has, and the window had no icon _ICON_PATH = Path(__file__).with_name("pybreeze_icon.ico") @@ -239,7 +236,12 @@ def start_editor(debug_mode: bool = False, theme: str | None = None, **kwargs) - def open_main_window(app: QApplication, debug_mode: bool = False, theme: str | None = None, **kwargs) -> PyBreezeMainWindow: - """Build the main window, style it, show it and apply the saved settings. + """Build the main window, apply a theme given here, and show it. + + JEditor's constructor applies the saved settings, the theme picked from UI + Style among them (``startup_setting()``). They are applied again only for a + theme given here, which becomes the picked one: applying a theme takes most + of a second, and the start applied the same one three times. :param app: the running application, which the theme is applied to :param debug_mode: close by itself after a while, as the startup tests need @@ -248,16 +250,24 @@ def open_main_window(app: QApplication, debug_mode: bool = False, theme: str | N :return: the window, which the caller keeps for as long as the IDE runs """ window = PyBreezeMainWindow(debug_mode=debug_mode, **kwargs) - # startup_setting() applies the picked theme, over any applied before it: - # a theme given here was never seen until it became the picked one if theme is not None: - user_setting_dict["ui_style"] = theme - apply_stylesheet(app, theme=user_setting_dict.get("ui_style", DEFAULT_THEME)) + _apply_given_theme(app, window, theme) + else: + # startup_setting() sets the window's font style sheet before the + # application's theme; set again after it, as a second run of it did, + # the toolbar keeps the height it has with a theme given (4 px less) + window.setStyleSheet(window.styleSheet()) window.showMaximized() + return window + + +def _apply_given_theme(app: QApplication, window: PyBreezeMainWindow, theme: str) -> None: + """Make *theme* the picked one and apply the settings with it (``startup_setting()``).""" + user_setting_dict["ui_style"] = theme try: window.startup_setting() # The user's saved settings, and the files they reopen, can be anything: - # a bad one is logged and the IDE starts without it. + # a bad one is logged, and the theme is still applied. except (OSError, ValueError, TypeError, KeyError, RuntimeError) as error: pybreeze_logger.error("Startup setting error: %r", error) - return window + apply_stylesheet(app, theme=theme) diff --git a/test/test_utils/test_jeditor_contract.py b/test/test_utils/test_jeditor_contract.py index cbedc2c1..f3148c62 100644 --- a/test/test_utils/test_jeditor_contract.py +++ b/test/test_utils/test_jeditor_contract.py @@ -93,6 +93,12 @@ def test_the_run_menu_keeps_its_stop_all_action(self): build = importlib.import_module("je_editor.pyside_ui.main_ui.menu.run_menu.build_run_menu") assert "run_menu.stop_all_program_action" in inspect.getsource(build) + def test_the_window_is_built_with_the_saved_settings(self): + from je_editor import EditorMain + + # open_main_window() applies them again only for a theme start_editor() is given + assert "self.startup_setting()" in inspect.getsource(EditorMain.__init__) + def test_the_startup_applies_the_saved_theme(self): from je_editor import EditorMain diff --git a/test/test_utils/test_start_theme.py b/test/test_utils/test_start_theme.py index f0422ba2..1e2c6411 100644 --- a/test/test_utils/test_start_theme.py +++ b/test/test_utils/test_start_theme.py @@ -41,3 +41,51 @@ def test_without_one_the_theme_picked_before_is_shown(tmp_path): def test_without_either_the_default_is_shown(tmp_path): assert _start(tmp_path)["shown"] == "dark_amber.xml" + + +_COUNT_THEMES = """ +applied = [] +_set_style_sheet = QApplication.setStyleSheet +def _counting(self, sheet): + applied.append(len(sheet)) + return _set_style_sheet(self, sheet) +QApplication.setStyleSheet = _counting +""" + +_READ_COUNT = """ +from PySide6.QtWidgets import QToolBar +for _ in range(5): + app.processEvents() +result = {"shown": os.environ.get("QTMATERIAL_THEME"), "applied": len(applied), + "toolbar": window.findChildren(QToolBar)[0].height()} +""" + + +def _count(tmp_path, theme: str | None = None, saved: str = "light_blue.xml") -> dict: + return run_started_window( + tmp_path, _READ_COUNT, before_window=_COUNT_THEMES, + build=_OPEN.format(theme="" if theme is None else f", theme={theme!r}"), + saved_settings={"ui_style": saved}) + + +def test_the_saved_theme_is_applied_once(tmp_path): + # The window applies the saved settings as it is built; they were applied + # twice more after it (about 0.7 s and 0.9 s of the start) + seen = _count(tmp_path) + assert (seen["shown"], seen["applied"]) == ("light_blue.xml", 1) + + +def test_a_theme_given_at_launch_is_applied_once_over_the_saved_one(tmp_path): + seen = _count(tmp_path, theme="dark_teal.xml") + assert (seen["shown"], seen["applied"]) == ("dark_teal.xml", 2) + + +def test_the_window_looks_the_same_with_the_theme_given_or_saved(tmp_path): + # Applied once, the theme left the toolbar 4 px taller than a theme given + # at launch, which applies the settings again + for folder in ("saved", "given"): + (tmp_path / folder).mkdir() + saved = _count(tmp_path / "saved", saved="dark_amber.xml") + given = _count(tmp_path / "given", theme="dark_amber.xml", saved="dark_amber.xml") + + assert saved["toolbar"] == given["toolbar"] From df1098ca7f9ceaba8674e791c2f8595f2585d457 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 17:59:54 +0800 Subject: [PATCH 160/166] Rename the attributes that hid QWidget.actions() and QObject.thread() --- CLAUDE.md | 1 + architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 +++ docs/updates/README.md | 3 +- .../code_review/cot_code_review_gui.py | 14 ++--- .../extend_ai_gui/skills/skills_send_gui.py | 18 +++--- .../jupyter_lab_gui/jupyter_lab_widget.py | 18 +++--- .../pybreeze_ui/tools_gui/curl_import_gui.py | 4 +- pybreeze/pybreeze_ui/tools_gui/diff_gui.py | 4 +- .../pybreeze_ui/tools_gui/har_import_gui.py | 4 +- pybreeze/pybreeze_ui/tools_gui/hash_gui.py | 4 +- .../tools_gui/header_analyzer_gui.py | 4 +- .../pybreeze_ui/tools_gui/http_status_gui.py | 4 +- .../pybreeze_ui/tools_gui/json_format_gui.py | 4 +- .../pybreeze_ui/tools_gui/jwt_decoder_gui.py | 4 +- .../pybreeze_ui/tools_gui/query_json_gui.py | 4 +- pybreeze/pybreeze_ui/tools_gui/regex_gui.py | 4 +- .../tools_gui/response_inspector_gui.py | 4 +- .../pybreeze_ui/tools_gui/timestamp_gui.py | 4 +- .../pybreeze_ui/tools_gui/url_builder_gui.py | 4 +- test/test_utils/test_ai_gui_lifecycle.py | 12 ++-- .../test_closed_panels_are_freed.py | 4 +- test/test_utils/test_cot_session_reuse.py | 8 +-- test/test_utils/test_curl_import_gui.py | 22 ++++---- test/test_utils/test_diff_gui.py | 2 +- test/test_utils/test_har_import_gui.py | 18 +++--- test/test_utils/test_hash_gui.py | 2 +- test/test_utils/test_header_analyzer_gui.py | 4 +- test/test_utils/test_http_status_gui.py | 4 +- test/test_utils/test_json_format_gui.py | 2 +- test/test_utils/test_jupyter_lifecycle.py | 4 +- test/test_utils/test_jwt_decoder_gui.py | 2 +- .../test_utils/test_no_qt_member_shadowing.py | 56 +++++++++++++++++++ test/test_utils/test_query_json_gui.py | 2 +- test/test_utils/test_regex_gui.py | 2 +- .../test_utils/test_response_inspector_gui.py | 2 +- test/test_utils/test_timestamp_gui.py | 6 +- test/test_utils/test_url_builder_gui.py | 2 +- 38 files changed, 168 insertions(+), 102 deletions(-) create mode 100644 test/test_utils/test_no_qt_member_shadowing.py diff --git a/CLAUDE.md b/CLAUDE.md index 18e8c3c9..9742eab5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,7 @@ ruff check pybreeze/ # before committing non-trivia - A context menu or dialog built on each use with a parent (`QMenu(self)`, `SomeDialog(self)`) is deleted once `exec()` returns (`deleteLater()`, or `WA_DeleteOnClose` for a message box): its parent keeps it otherwise, one more per use - A process the IDE starts gets `child_environment()` or `utf8_subprocess_env()` (`utils/subprocess_util.py`) as its `env`, never `os.environ` as it is: a variable the IDE sets for itself alone has the value `IDE_ONLY` and stays out (`LOCUST_SKIP_MONKEY_PATCH`, which a load test must not inherit) - Import `je_auto_control` only where it is used, never at the top of a module the IDE loads as it starts: it makes the process system DPI aware as it imports, which keeps Qt from making the IDE per-monitor aware. The automation packages' GUIs and the SSH client (paramiko) are likewise imported by the entry that opens them, which keeps almost two seconds off the start; `test_startup_imports.py` fails when one of them is imported as the IDE starts +- An instance attribute of a Qt class never takes the name of a member of its Qt base (`self.actions`, `self.thread`, `self.layout`, …): it hides the method from everything that calls it on the widget. `test_no_qt_member_shadowing.py` fails on one - Delete unused code immediately — no dead imports, unreachable branches, commented-out blocks, or `_old_` prefixes - Follow PEP 8 and standard Pythonic practice; `ruff` is the arbiter diff --git a/architecture_explore.md b/architecture_explore.md index 4f00257e..f9785d2b 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 149 個 `test_*.py`、2711 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 150 個 `test_*.py`、2712 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 0db127d2..50186038 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -902,3 +902,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/editor_main/main_ui.py`, `test/test_utils/test_start_theme.py`, `test/test_utils/test_jeditor_contract.py`, `architecture.md` (§4 flow, §6), `architecture_explore.md` (§3 start, test count) - **Checks**: `open_main_window()` before (378c078) and after, alternated five times each: median 3.32 s → 1.07 s. Images of the started window (offscreen, 1772 × 796) with `light_blue.xml` saved and with nothing saved are identical pixel for pixel before and after. The start and extend startup tests exit 0. `test/test_utils` collects 2711 tests in 149 files. - **Open items**: none. + +## U-20260925-108 · 2026-09-25 · No tool tab or panel hides a Qt method behind an attribute of the same name · #refactor #quality + +- **What**: a type check of the package (mypy, run for the first time here) reported `button_row` missing on a callable in thirteen tool tabs and `status_update` on one in the JupyterLab tab. They kept their output buttons in `self.actions` and their worker in `self.thread`, which hide `QWidget.actions()` and `QObject.thread()`: anything calling those on the widget (a plugin, a later helper, JEditor collecting a widget's actions) got PyBreeze's object back. Nothing does today, so nothing was broken. A check of every Qt class in the package against the members of its Qt bases found these sixteen and no others. +- **Change** (refactor, no behaviour change): `self.actions` → `self.output_actions` in the thirteen tool tabs; `self.thread` → `self.request_thread` in the CoT review and Skill send panels, `self.launcher` in the JupyterLab tab; the tests that reach them follow. `test_no_qt_member_shadowing.py` (new) fails on an instance attribute of a PyBreeze Qt class named after a member of its Qt base, and `CLAUDE.md` (Conventions) says so. The other mypy reports were checked and left: types it cannot narrow (a line edit or a combo box by branch, optional attributes set before use), and a PySide6 stub that types `QFile.moveToTrash()` as a tuple where it returns a `bool` (checked: `False` for a missing file). +- **Files**: the thirteen `pybreeze/pybreeze_ui/tools_gui/*_gui.py`, `extend_ai_gui/code_review/cot_code_review_gui.py`, `extend_ai_gui/skills/skills_send_gui.py`, `jupyter_lab_gui/jupyter_lab_widget.py`, their tests, `test/test_utils/test_no_qt_member_shadowing.py` (new), `CLAUDE.md`, `architecture_explore.md` (test count) +- **Checks**: the tool tab, AI panel, JupyterLab and Tools-menu tests pass (279); the new test passes, and the same check on the code before the rename listed the sixteen. `test/test_utils` collects 2712 tests in 150 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7f343d78..7fc10ec0 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-108 | 2026-09-25 | No tool tab or panel hides a Qt method behind an attribute of the same name | #refactor #quality | [2026-09-c](2026-09-c.md) | | U-20260925-107 | 2026-09-25 | Opening the main window takes about 1 s instead of 3: the theme is applied once | #perf #startup | [2026-09-c](2026-09-c.md) | | U-20260925-106 | 2026-09-25 | The packaged build's regex worker: its timeout and its errors are tested | #test #regex | [2026-09-c](2026-09-c.md) | | U-20260925-105 | 2026-09-25 | The main window shows PyBreeze's icon wherever the IDE starts from | #fix #packaging #ui | [2026-09-c](2026-09-c.md) | @@ -510,4 +511,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 107 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 108 | diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py index 4283b04b..fd50a867 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py @@ -72,7 +72,7 @@ def __init__(self): # 儲存回覆 self.responses = {} - self.thread = None + self.request_thread = None def show_response(self, filename): if filename in self.responses: @@ -96,7 +96,7 @@ def start_sending(self): # Ignore re-submits while a run is in flight so we never drop a running # QThread or interleave two review passes into the same response store. - if self.thread is not None and self.thread.isRunning(): + if self.request_thread is not None and self.request_thread.isRunning(): return # A new run starts from nothing: answers about the previous code, or its @@ -107,10 +107,10 @@ def start_sending(self): # 啟動傳送 Thread self.send_button.setEnabled(False) - self.thread = SenderThread(files=self.files, code=code, url=url) - self.thread.update_response.connect(self.handle_response) - self.thread.finished.connect(self._enable_send) - self.thread.start() + self.request_thread = SenderThread(files=self.files, code=code, url=url) + self.request_thread.update_response.connect(self.handle_response) + self.request_thread.finished.connect(self._enable_send) + self.request_thread.start() def _enable_send(self) -> None: """Let the next request be sent, however this one ended. @@ -136,7 +136,7 @@ def closeEvent(self, event): that long. The thread is cut off from this widget and kept until it ends: a QThread destroyed while running aborts the process. """ - thread = self.thread + thread = self.request_thread if thread is not None and thread.isRunning(): thread.requestInterruption() let_run_out(thread, thread.update_response) diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py index db29e720..3a308e61 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py @@ -148,7 +148,7 @@ def __init__(self): self.setLayout(layout) - self.thread = None # 保存執行緒 + self.request_thread = None # 保存執行緒 # 編輯區裡是哪個模板:換模板被拒時選單要回到這裡 # The template in the edit area: where the selector goes back to when a switch is refused self._shown_template = self.prompt_select.currentText() @@ -195,10 +195,10 @@ def _may_replace_edits(self, name: str) -> bool: return reply == QMessageBox.StandardButton.Yes def send_prompt(self): - # Ignore re-submits while a request is in flight: reassigning self.thread + # Ignore re-submits while a request is in flight: reassigning self.request_thread # here would drop a still-running QThread (risking "destroyed while # running") and let a stale worker overwrite the panel. - if self.thread is not None and self.thread.isRunning(): + if self.request_thread is not None and self.request_thread.isRunning(): return api_url = self.api_url_input.text().strip() @@ -218,13 +218,13 @@ def send_prompt(self): # 啟動 QThread self.send_button.setEnabled(False) - self.thread = RequestThread(api_url, prompt_text) - self.thread.answered.connect(self.on_finished) - self.thread.error.connect(self.on_error) + self.request_thread = RequestThread(api_url, prompt_text) + self.request_thread.answered.connect(self.on_finished) + self.request_thread.error.connect(self.on_error) # However run() ends -- including an exception outside its handler -- # the button comes back. - self.thread.finished.connect(self._enable_send) - self.thread.start() + self.request_thread.finished.connect(self._enable_send) + self.request_thread.start() def _enable_send(self) -> None: """Let the next request be sent, however this one ended. @@ -251,7 +251,7 @@ def closeEvent(self, event): panel does not wait for it: the request can take the whole read timeout, and waiting froze the IDE for that long. """ - thread = self.thread + thread = self.request_thread if thread is not None and thread.isRunning(): let_run_out(thread, thread.answered, thread.error) event.accept() diff --git a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py index 2654714f..7b3c55e8 100644 --- a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py +++ b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py @@ -33,11 +33,11 @@ def __init__(self, python_exe: str | None = None): self.browser.hide() layout.addWidget(self.browser) - self.thread = JupyterLauncherThread(python_exe=python_exe) - self.thread.status_update.connect(self.update_status) - self.thread.server_ready.connect(self.load_lab) - self.thread.error_occurred.connect(self.show_error) - self.thread.start() + self.launcher = JupyterLauncherThread(python_exe=python_exe) + self.launcher.status_update.connect(self.update_status) + self.launcher.server_ready.connect(self.load_lab) + self.launcher.error_occurred.connect(self.show_error) + self.launcher.start() def update_status(self, text): # status_label is removed once the lab loads; a late status/error signal @@ -77,17 +77,17 @@ def closeEvent(self, event): JupyterLab process behind for every tab that had finished loading -- holding its port, and reachable for as long as the machine was up. """ - if self.thread.isRunning(): + if self.launcher.isRunning(): # Still installing or starting: cut off from this tab first, so a # late status or error cannot reach it, and kept until it ends # rather than waited for -- an install can take minutes, and a # QThread destroyed while running aborts the process. (Not # blockSignals: that would also block the ``finished`` that lets # the keeper release it.) - let_run_out(self.thread, self.thread.status_update, - self.thread.server_ready, self.thread.error_occurred) + let_run_out(self.launcher, self.launcher.status_update, + self.launcher.server_ready, self.launcher.error_occurred) # After this the launcher starts no server, even one still installing. - self.thread.stop() + self.launcher.stop() event.accept() diff --git a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py index f5673cb6..b2339df1 100644 --- a/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/curl_import_gui.py @@ -85,7 +85,7 @@ def __init__(self, main_window=None) -> None: # Shared copy / open-in-editor / save actions. The extension and basename # follow the selected target; open/save are no-ops until a valid template. - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename=lambda: "action" if self.selected_target() == _JSON_TARGET else "request", extension=lambda: "json" if self.selected_target() == _JSON_TARGET else "py", @@ -99,7 +99,7 @@ def __init__(self, main_window=None) -> None: ): layout.addWidget(widget) layout.addLayout(cross_tool) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def selected_target(self) -> str: diff --git a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py index 269ae4d3..da377104 100644 --- a/pybreeze/pybreeze_ui/tools_gui/diff_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/diff_gui.py @@ -116,7 +116,7 @@ def __init__(self, main_window=None) -> None: self.output_edit.setReadOnly(True) self._highlighter = UnifiedDiffHighlighter(self.output_edit.document()) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="diff", extension="txt") @@ -125,7 +125,7 @@ def __init__(self, main_window=None) -> None: layout.addWidget(self.compare_button) layout.addWidget(self.summary_label) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) self._compare_thread: DiffThread | None = None diff --git a/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py b/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py index 8ff80651..1340e634 100644 --- a/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/har_import_gui.py @@ -91,7 +91,7 @@ def __init__(self, main_window=None) -> None: use_fixed_pitch_font(self.output_edit) self.output_edit.setReadOnly(True) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename=lambda: "actions" if self.selected_target() == _JSON_TARGET else "session", extension=lambda: "json" if self.selected_target() == _JSON_TARGET else "py", @@ -106,7 +106,7 @@ def __init__(self, main_window=None) -> None: layout.addLayout(generate_row) layout.addWidget(self.output_label) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def selected_target(self) -> str: diff --git a/pybreeze/pybreeze_ui/tools_gui/hash_gui.py b/pybreeze/pybreeze_ui/tools_gui/hash_gui.py index 3f41bbdf..229beaf6 100644 --- a/pybreeze/pybreeze_ui/tools_gui/hash_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/hash_gui.py @@ -42,7 +42,7 @@ def __init__(self, main_window=None) -> None: self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="hashes", extension="txt") @@ -52,7 +52,7 @@ def __init__(self, main_window=None) -> None: self.output_label, self.output_edit, ): layout.addWidget(widget) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def compute(self) -> None: diff --git a/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py b/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py index b93c1b35..9a747fac 100644 --- a/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/header_analyzer_gui.py @@ -88,7 +88,7 @@ def __init__(self, main_window=None, initial_headers: str | None = None) -> None self.open_jwt_button.clicked.connect(self.open_jwt_in_decoder) self.open_jwt_button.setEnabled(False) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="headers", extension="txt", is_valid=lambda: self._analysis is not None) @@ -99,7 +99,7 @@ def __init__(self, main_window=None, initial_headers: str | None = None) -> None self.output_label, self.output_edit, self.open_jwt_button, ): layout.addWidget(widget) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) if initial_headers: diff --git a/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py b/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py index 979eb363..d430f016 100644 --- a/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/http_status_gui.py @@ -61,7 +61,7 @@ def __init__(self, initial_search: str = "", main_window=None) -> None: self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="http_status", extension="txt") @@ -69,7 +69,7 @@ def __init__(self, initial_search: str = "", main_window=None) -> None: layout.addWidget(self.search_label) layout.addWidget(self.search_edit) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) # Setting the text triggers refresh; an empty value shows the whole table. diff --git a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py index 85ce4720..c48afa4c 100644 --- a/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/json_format_gui.py @@ -50,7 +50,7 @@ def __init__(self, main_window=None, initial_json: str | None = None) -> None: use_fixed_pitch_font(self.output_edit) self.output_edit.setReadOnly(True) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="formatted", extension="json", is_valid=lambda: self._valid_output) @@ -61,7 +61,7 @@ def __init__(self, main_window=None, initial_json: str | None = None) -> None: layout.addLayout(buttons) layout.addWidget(self.output_label) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) if initial_json: diff --git a/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py b/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py index a020b9fa..e9d0d52c 100644 --- a/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/jwt_decoder_gui.py @@ -70,7 +70,7 @@ def __init__(self, initial_token: str | None = None, main_window=None) -> None: self.output_edit.setReadOnly(True) use_fixed_pitch_font(self.output_edit) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="jwt", extension="txt", is_valid=lambda: self._valid_output) @@ -80,7 +80,7 @@ def __init__(self, initial_token: str | None = None, main_window=None) -> None: self.output_label, self.output_edit, ): layout.addWidget(widget) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) if initial_token: diff --git a/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py b/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py index 6242cb64..325a147c 100644 --- a/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/query_json_gui.py @@ -51,7 +51,7 @@ def __init__(self, main_window=None) -> None: self.output_edit.setReadOnly(True) use_fixed_pitch_font(self.output_edit) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="query", extension="txt", is_valid=lambda: self._valid_output) @@ -61,7 +61,7 @@ def __init__(self, main_window=None) -> None: layout.addLayout(buttons) layout.addWidget(self.output_label) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def convert_as_pasted(self) -> None: diff --git a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py index 8f6f2965..93eb238a 100644 --- a/pybreeze/pybreeze_ui/tools_gui/regex_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/regex_gui.py @@ -101,7 +101,7 @@ def __init__(self, main_window=None) -> None: self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="matches", extension="txt", is_valid=lambda: self._valid_output) @@ -114,7 +114,7 @@ def __init__(self, main_window=None) -> None: layout.addWidget(self.test_button) layout.addWidget(self.output_label) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def selected_flags(self) -> list[str]: diff --git a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py index 673ab5b0..aff46672 100644 --- a/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/response_inspector_gui.py @@ -118,7 +118,7 @@ def __init__(self, main_window=None) -> None: self.open_body_button.setEnabled(False) # Shared copy / open-in-editor / save actions, valid once analysed. - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="response", extension="txt", is_valid=lambda: self._analysis is not None) @@ -136,7 +136,7 @@ def __init__(self, main_window=None) -> None: layout.addWidget(self.output_label) layout.addWidget(self.output_edit) layout.addLayout(cross_tool) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def analyze(self) -> None: diff --git a/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py b/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py index 6b850665..8914735b 100644 --- a/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/timestamp_gui.py @@ -53,7 +53,7 @@ def __init__(self, main_window=None) -> None: self.output_edit = QTextEdit() self.output_edit.setReadOnly(True) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="timestamp", extension="txt", is_valid=lambda: self._valid_output) @@ -63,7 +63,7 @@ def __init__(self, main_window=None) -> None: self.output_label, self.output_edit, ): layout.addWidget(widget) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) def convert(self) -> None: diff --git a/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py b/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py index 5b653db0..044dd13d 100644 --- a/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py +++ b/pybreeze/pybreeze_ui/tools_gui/url_builder_gui.py @@ -52,7 +52,7 @@ def __init__(self, main_window=None, initial_url: str | None = None) -> None: self.output_edit.setReadOnly(True) use_fixed_pitch_font(self.output_edit) - self.actions = OutputActions( + self.output_actions = OutputActions( self, self.output_edit, main_window=main_window, basename="url", extension="txt", is_valid=lambda: self._valid_output) @@ -62,7 +62,7 @@ def __init__(self, main_window=None, initial_url: str | None = None) -> None: layout.addLayout(buttons) layout.addWidget(self.output_label) layout.addWidget(self.output_edit) - layout.addLayout(self.actions.button_row()) + layout.addLayout(self.output_actions.button_row()) self.setLayout(layout) if initial_url: diff --git a/test/test_utils/test_ai_gui_lifecycle.py b/test/test_utils/test_ai_gui_lifecycle.py index d234e3b0..16969788 100644 --- a/test/test_utils/test_ai_gui_lifecycle.py +++ b/test/test_utils/test_ai_gui_lifecycle.py @@ -35,7 +35,7 @@ def test_a_running_review_is_interrupted_and_let_go_of_without_waiting(self): gui = CoTCodeReviewGUI.__new__(CoTCodeReviewGUI) thread = _running_thread() - gui.thread = thread + gui.request_thread = thread event = MagicMock() CoTCodeReviewGUI.closeEvent(gui, event) @@ -49,7 +49,7 @@ def test_a_running_review_is_interrupted_and_let_go_of_without_waiting(self): def test_no_thread_just_accepts(self): gui = CoTCodeReviewGUI.__new__(CoTCodeReviewGUI) - gui.thread = None + gui.request_thread = None event = MagicMock() CoTCodeReviewGUI.closeEvent(gui, event) @@ -60,7 +60,7 @@ def test_finished_thread_is_not_awaited(self): gui = CoTCodeReviewGUI.__new__(CoTCodeReviewGUI) thread = MagicMock() thread.isRunning.return_value = False - gui.thread = thread + gui.request_thread = thread event = MagicMock() CoTCodeReviewGUI.closeEvent(gui, event) @@ -78,7 +78,7 @@ def test_a_running_request_is_let_go_of_without_waiting(self): gui = SkillsSendGUI.__new__(SkillsSendGUI) thread = _running_thread() - gui.thread = thread + gui.request_thread = thread event = MagicMock() SkillsSendGUI.closeEvent(gui, event) @@ -115,7 +115,7 @@ def test_its_answer_signal_does_not_hide_the_thread_ending(self): def test_no_thread_just_accepts(self): gui = SkillsSendGUI.__new__(SkillsSendGUI) - gui.thread = None + gui.request_thread = None event = MagicMock() SkillsSendGUI.closeEvent(gui, event) @@ -177,6 +177,6 @@ def test_nothing_is_sent_and_the_user_is_told(self, qapp, monkeypatch): gui.start_sending() - assert gui.thread is None + assert gui.request_thread is None assert shown == [cot_mod.language_wrapper.language_word_dict.get("cot_gui_error_no_code")] gui.deleteLater() diff --git a/test/test_utils/test_closed_panels_are_freed.py b/test/test_utils/test_closed_panels_are_freed.py index 5f9be74b..6d2d215f 100644 --- a/test/test_utils/test_closed_panels_are_freed.py +++ b/test/test_utils/test_closed_panels_are_freed.py @@ -102,13 +102,13 @@ def _diff(widget) -> None: def test_the_cot_review_panel(app): from pybreeze.pybreeze_ui.extend_ai_gui.code_review.cot_code_review_gui import CoTCodeReviewGUI - assert _freed_after_a_run(app, CoTCodeReviewGUI, _cot, lambda widget: widget.thread) + assert _freed_after_a_run(app, CoTCodeReviewGUI, _cot, lambda widget: widget.request_thread) def test_the_skill_send_panel(app): from pybreeze.pybreeze_ui.extend_ai_gui.skills.skills_send_gui import SkillsSendGUI - assert _freed_after_a_run(app, SkillsSendGUI, _skills, lambda widget: widget.thread) + assert _freed_after_a_run(app, SkillsSendGUI, _skills, lambda widget: widget.request_thread) def test_the_diff_tab(app): diff --git a/test/test_utils/test_cot_session_reuse.py b/test/test_utils/test_cot_session_reuse.py index 30479dc3..2bb51e38 100644 --- a/test/test_utils/test_cot_session_reuse.py +++ b/test/test_utils/test_cot_session_reuse.py @@ -179,9 +179,9 @@ def test_closing_mid_review_does_not_wait(monkeypatch): answering = threading.Event() monkeypatch.setattr(code_review_thread.SenderThread, "run", lambda self: answering.wait(5)) gui = CoTCodeReviewGUI() - gui.thread = code_review_thread.SenderThread(files=[], code="", url="https://review.example") - gui.thread.start() - thread = gui.thread + gui.request_thread = code_review_thread.SenderThread(files=[], code="", url="https://review.example") + gui.request_thread.start() + thread = gui.request_thread gui.close() # returns while the request is still out @@ -279,7 +279,7 @@ def test_sending_resolves_nothing_on_the_ui_thread(monkeypatch): gui.start_sending() assert looked_up == [] - assert gui.thread is not None + assert gui.request_thread is not None gui.deleteLater() diff --git a/test/test_utils/test_curl_import_gui.py b/test/test_utils/test_curl_import_gui.py index fa23e110..5b35d632 100644 --- a/test/test_utils/test_curl_import_gui.py +++ b/test/test_utils/test_curl_import_gui.py @@ -60,12 +60,12 @@ def test_empty_input_shows_hint(self, widget): def test_copy_output_sets_clipboard(self, app, widget): widget.input_edit.setPlainText("curl https://example.com/api") widget.convert() - widget.actions.copy() + widget.output_actions.copy() assert "import requests" in QApplication.clipboard().text() def test_copy_with_empty_output_is_safe(self, widget): widget.output_edit.setPlainText("") - widget.actions.copy() # must not raise + widget.output_actions.copy() # must not raise def test_json_body_conversion(self, widget): widget.input_edit.setPlainText( @@ -149,34 +149,34 @@ def test_open_in_editor_adds_tab_with_code(self, widget_with_window): with patch( "je_editor.EditorWidget", _FakeEditor ): - editor = gui.actions.open_in_editor() + editor = gui.output_actions.open_in_editor() assert len(window.tab_widget.added) == 1 assert "import requests" in editor.code_edit.text def test_open_in_editor_without_code_is_noop(self, widget_with_window): gui, window = widget_with_window - assert gui.actions.open_in_editor() is None + assert gui.output_actions.open_in_editor() is None assert window.tab_widget.added == [] def test_open_after_parse_error_is_noop(self, widget_with_window): gui, window = widget_with_window gui.input_edit.setPlainText("wget https://x") gui.convert() - assert gui.actions.open_in_editor() is None + assert gui.output_actions.open_in_editor() is None assert window.tab_widget.added == [] def test_open_in_editor_without_window_is_safe(self, widget): widget.input_edit.setPlainText("curl https://x") widget.convert() - assert widget.actions.open_in_editor() is None # no main window + assert widget.output_actions.open_in_editor() is None # no main window def test_suggested_filename_python(self, widget): self._select_target(widget, "requests") - assert widget.actions.suggested_filename() == "request.py" + assert widget.output_actions.suggested_filename() == "request.py" def test_suggested_filename_json(self, widget): self._select_target(widget, "apitestka_action") - assert widget.actions.suggested_filename() == "action.json" + assert widget.output_actions.suggested_filename() == "action.json" def test_save_to_file_writes(self, widget, tmp_path): widget.input_edit.setPlainText("curl https://x") @@ -186,7 +186,7 @@ def test_save_to_file_writes(self, widget, tmp_path): "pybreeze.pybreeze_ui.tools_gui.output_actions.QFileDialog.getSaveFileName", return_value=(str(target), "Python (*.py)"), ): - result = widget.actions.save_to_file() + result = widget.output_actions.save_to_file() assert result == str(target) assert "import requests" in target.read_text(encoding="utf-8") @@ -197,12 +197,12 @@ def test_save_cancelled_returns_none(self, widget, tmp_path): "pybreeze.pybreeze_ui.tools_gui.output_actions.QFileDialog.getSaveFileName", return_value=("", ""), ): - assert widget.actions.save_to_file() is None + assert widget.output_actions.save_to_file() is None def test_save_after_parse_error_is_noop(self, widget): widget.input_edit.setPlainText("wget https://x") widget.convert() - assert widget.actions.save_to_file() is None + assert widget.output_actions.save_to_file() is None class TestCurlImportOpenUrlInBuilder: diff --git a/test/test_utils/test_diff_gui.py b/test/test_utils/test_diff_gui.py index 4b4c3748..db138e07 100644 --- a/test/test_utils/test_diff_gui.py +++ b/test/test_utils/test_diff_gui.py @@ -68,7 +68,7 @@ def test_copy_output(self, app, widget): widget.left_edit.setPlainText("a") widget.right_edit.setPlainText("b") _compare(widget) - widget.actions.copy() + widget.output_actions.copy() assert QApplication.clipboard().text() != "" def test_build_summary_line_identical(self, app): diff --git a/test/test_utils/test_har_import_gui.py b/test/test_utils/test_har_import_gui.py index 72c3b39f..68af0712 100644 --- a/test/test_utils/test_har_import_gui.py +++ b/test/test_utils/test_har_import_gui.py @@ -130,17 +130,17 @@ def test_action_target_writes_one_action_list(self, loaded): def test_suggested_filename_follows_the_target(self, loaded): self._select_target(loaded, "apitestka_action") - assert loaded.actions.suggested_filename() == "actions.json" + assert loaded.output_actions.suggested_filename() == "actions.json" self._select_target(loaded, "pytest") - assert loaded.actions.suggested_filename() == "session.py" + assert loaded.output_actions.suggested_filename() == "session.py" def test_copy_output(self, app, loaded): loaded.generate_all() - loaded.actions.copy() + loaded.output_actions.copy() assert "/v1/items" in QApplication.clipboard().text() def test_save_before_generating_is_noop(self, loaded): - assert loaded.actions.save_to_file() is None + assert loaded.output_actions.save_to_file() is None class TestHarImportFileDialog: @@ -231,7 +231,7 @@ def test_loading_another_file_clears_the_previous_script(self, loaded): assert loaded.load_text(other) assert loaded.output_edit.toPlainText() == "" - assert not loaded.actions._has_output() + assert not loaded.output_actions._has_output() def test_a_file_that_cannot_be_read_unlists_the_previous_one(self, loaded, tmp_path): with patch( @@ -243,7 +243,7 @@ def test_a_file_that_cannot_be_read_unlists_the_previous_one(self, loaded, tmp_p assert loaded.entry_list.count() == 0 loaded.generate_all() assert "/v1/items" not in loaded.output_edit.toPlainText() - assert not loaded.actions._has_output() + assert not loaded.output_actions._has_output() def test_choosing_another_target_generates_again(self, loaded): loaded.generate_all() @@ -251,7 +251,7 @@ def test_choosing_another_target_generates_again(self, loaded): loaded.target_select.setCurrentIndex(loaded.target_select.findData("apitestka_action")) assert len(json.loads(loaded.output_edit.toPlainText())) == 2 - assert loaded.actions.suggested_filename() == "actions.json" + assert loaded.output_actions.suggested_filename() == "actions.json" def test_choosing_a_target_before_generating_generates_nothing(self, loaded): loaded.target_select.setCurrentIndex(loaded.target_select.findData("pytest")) @@ -271,8 +271,8 @@ def test_going_back_from_a_target_that_failed_generates_again(self, widget): requests_script = widget.output_edit.toPlainText() widget.target_select.setCurrentIndex(widget.target_select.findData("apitestka_action")) - assert not widget.actions._has_output() + assert not widget.output_actions._has_output() widget.target_select.setCurrentIndex(widget.target_select.findData("requests")) assert widget.output_edit.toPlainText() == requests_script - assert widget.actions._has_output() + assert widget.output_actions._has_output() diff --git a/test/test_utils/test_hash_gui.py b/test/test_utils/test_hash_gui.py index b596bade..a2eabcfd 100644 --- a/test/test_utils/test_hash_gui.py +++ b/test/test_utils/test_hash_gui.py @@ -46,7 +46,7 @@ def test_empty_input_still_hashes(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setPlainText("hello") widget.compute() - widget.actions.copy() + widget.output_actions.copy() assert hashlib.sha256(b"hello").hexdigest() in QApplication.clipboard().text() def test_build_hash_text_formats_lines(self): diff --git a/test/test_utils/test_header_analyzer_gui.py b/test/test_utils/test_header_analyzer_gui.py index ee335608..1fbc4ce4 100644 --- a/test/test_utils/test_header_analyzer_gui.py +++ b/test/test_utils/test_header_analyzer_gui.py @@ -86,13 +86,13 @@ def test_initial_headers_are_analysed_on_open(self, app): def test_copy_output(self, app, widget): widget.input_edit.setPlainText(_RESPONSE) widget.analyze() - widget.actions.copy() + widget.output_actions.copy() assert "nginx/1.25.3" in QApplication.clipboard().text() def test_save_after_no_headers_is_noop(self, widget): widget.input_edit.setPlainText("just some prose") widget.analyze() - assert widget.actions.save_to_file() is None + assert widget.output_actions.save_to_file() is None class _FakeTabWidget: diff --git a/test/test_utils/test_http_status_gui.py b/test/test_utils/test_http_status_gui.py index 41b6db84..5fca8f1b 100644 --- a/test/test_utils/test_http_status_gui.py +++ b/test/test_utils/test_http_status_gui.py @@ -67,8 +67,8 @@ def test_initial_search_prefills(self, app): def test_has_output_actions(self, widget): # The reference table is always content, so save/copy operate on it. - assert widget.actions.suggested_filename() == "http_status.txt" - widget.actions.copy() + assert widget.output_actions.suggested_filename() == "http_status.txt" + widget.output_actions.copy() assert "200 OK" in QApplication.clipboard().text() diff --git a/test/test_utils/test_json_format_gui.py b/test/test_utils/test_json_format_gui.py index fbcf6655..db75ffce 100644 --- a/test/test_utils/test_json_format_gui.py +++ b/test/test_utils/test_json_format_gui.py @@ -54,5 +54,5 @@ def test_empty_shows_hint(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setPlainText('{"a":1}') widget.minify() - widget.actions.copy() + widget.output_actions.copy() assert '{"a":1}' in QApplication.clipboard().text() diff --git a/test/test_utils/test_jupyter_lifecycle.py b/test/test_utils/test_jupyter_lifecycle.py index 633a3037..c8e844b5 100644 --- a/test/test_utils/test_jupyter_lifecycle.py +++ b/test/test_utils/test_jupyter_lifecycle.py @@ -157,7 +157,7 @@ def test_the_server_is_stopped_after_the_lab_has_loaded(self, app, monkeypatch): tab.close() - assert tab.thread.stopped + assert tab.launcher.stopped _CLOSE_WITH_A_TOOL_TAB_AND_DOCK = """ @@ -216,7 +216,7 @@ def test_closing_the_tab_does_not_wait_for_the_launcher(self, app, monkeypatch): monkeypatch.setattr( jupyter_lab_thread.JupyterLauncherThread, "run", lambda self: installing.wait(5)) tab = jupyter_lab_widget.JupyterLabWidget() - launcher = tab.thread + launcher = tab.launcher tab.close() # returns with the install still going diff --git a/test/test_utils/test_jwt_decoder_gui.py b/test/test_utils/test_jwt_decoder_gui.py index e1e4d23b..d71ee59a 100644 --- a/test/test_utils/test_jwt_decoder_gui.py +++ b/test/test_utils/test_jwt_decoder_gui.py @@ -63,7 +63,7 @@ def test_empty_input_shows_hint(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setPlainText(_make_jwt({"alg": "HS256"}, {"sub": "42"})) widget.decode() - widget.actions.copy() + widget.output_actions.copy() assert '"sub": "42"' in QApplication.clipboard().text() def test_build_decoded_text_without_timestamps(self, app): diff --git a/test/test_utils/test_no_qt_member_shadowing.py b/test/test_utils/test_no_qt_member_shadowing.py new file mode 100644 index 00000000..e68d3fa5 --- /dev/null +++ b/test/test_utils/test_no_qt_member_shadowing.py @@ -0,0 +1,56 @@ +"""No PyBreeze Qt class sets an instance attribute that hides a member of its Qt base. + +Thirteen tool tabs kept their output buttons in ``self.actions`` and three panels +their worker in ``self.thread``: ``widget.actions()`` and ``widget.thread()`` +then gave back that object, not what Qt's own methods return, to anything that +asked -- a plugin, a later helper, JEditor collecting a widget's actions. +""" +from __future__ import annotations + +import ast +import importlib +import inspect +import os +from pathlib import Path + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +from PySide6.QtCore import QObject +from PySide6.QtWidgets import QApplication + +_PACKAGE = Path(__file__).resolve().parents[2] / "pybreeze" + + +def _self_attributes(node: ast.ClassDef) -> set[str]: + return {target.attr for sub in ast.walk(node) if isinstance(sub, ast.Assign) + for target in sub.targets + if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) + and target.value.id == "self"} + + +def _shadowing(module_name: str, node: ast.ClassDef) -> list[str]: + cls = getattr(importlib.import_module(module_name), node.name, None) + if not inspect.isclass(cls) or not issubclass(cls, QObject): + return [] + qt_bases = [base for base in cls.__mro__ if base.__module__.startswith(("PySide6", "Shiboken"))] + found = [] + for name in sorted(_self_attributes(node)): + owner = next((base for base in qt_bases if name in vars(base)), None) + if owner is not None: + found.append(f"{module_name}.{node.name}: self.{name} hides {owner.__name__}.{name}") + return found + + +def test_no_instance_attribute_hides_a_qt_member(): + QApplication.instance() or QApplication([]) + found = [] + for path in sorted(_PACKAGE.rglob("*.py")): + classes = [node for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) + if isinstance(node, ast.ClassDef)] + if not classes: + continue + module_name = ".".join(path.relative_to(_PACKAGE.parent).with_suffix("").parts) + for node in classes: + found += _shadowing(module_name, node) + + assert found == [] diff --git a/test/test_utils/test_query_json_gui.py b/test/test_utils/test_query_json_gui.py index a111e9b7..5c03c234 100644 --- a/test/test_utils/test_query_json_gui.py +++ b/test/test_utils/test_query_json_gui.py @@ -58,7 +58,7 @@ def test_empty_to_query_shows_hint(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setPlainText("a=1&b=2") widget.convert_to_json() - widget.actions.copy() + widget.output_actions.copy() assert "a" in QApplication.clipboard().text() diff --git a/test/test_utils/test_regex_gui.py b/test/test_utils/test_regex_gui.py index f396feff..2a861654 100644 --- a/test/test_utils/test_regex_gui.py +++ b/test/test_utils/test_regex_gui.py @@ -86,7 +86,7 @@ def test_copy_output(self, app, widget): widget.pattern_edit.setText(r"\d+") widget.text_edit.setPlainText("a1") run(widget) - widget.actions.copy() + widget.output_actions.copy() assert "'1'" in QApplication.clipboard().text() def test_build_matches_text_no_matches(self, app): diff --git a/test/test_utils/test_response_inspector_gui.py b/test/test_utils/test_response_inspector_gui.py index 2dab883f..bd181bdf 100644 --- a/test/test_utils/test_response_inspector_gui.py +++ b/test/test_utils/test_response_inspector_gui.py @@ -65,7 +65,7 @@ def test_empty_shows_hint(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setPlainText('{"a": 1}') widget.analyze() - widget.actions.copy() + widget.output_actions.copy() assert '"a": 1' in QApplication.clipboard().text() def test_build_report_text_minimal(self, app): diff --git a/test/test_utils/test_timestamp_gui.py b/test/test_utils/test_timestamp_gui.py index 412ee037..def04bc1 100644 --- a/test/test_utils/test_timestamp_gui.py +++ b/test/test_utils/test_timestamp_gui.py @@ -54,7 +54,7 @@ def test_empty_shows_hint(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setText("1609459200") widget.convert() - widget.actions.copy() + widget.output_actions.copy() assert "1609459200" in QApplication.clipboard().text() def test_save_after_error_is_noop(self, widget, tmp_path): @@ -66,7 +66,7 @@ def test_save_after_error_is_noop(self, widget, tmp_path): "pybreeze.pybreeze_ui.tools_gui.output_actions.QFileDialog.getSaveFileName", return_value=(str(tmp_path / "x.txt"), "Text (*.txt)"), ): - assert widget.actions.save_to_file() is None + assert widget.output_actions.save_to_file() is None def test_suggested_filename(self, widget): - assert widget.actions.suggested_filename() == "timestamp.txt" + assert widget.output_actions.suggested_filename() == "timestamp.txt" diff --git a/test/test_utils/test_url_builder_gui.py b/test/test_utils/test_url_builder_gui.py index 831848bd..2d861dc1 100644 --- a/test/test_utils/test_url_builder_gui.py +++ b/test/test_utils/test_url_builder_gui.py @@ -60,7 +60,7 @@ def test_empty_to_url_shows_hint(self, widget): def test_copy_output(self, app, widget): widget.input_edit.setPlainText("https://example.com/api?a=1") widget.convert_to_json() - widget.actions.copy() + widget.output_actions.copy() assert "example.com" in QApplication.clipboard().text() From 0ecc4acb2d1253dfddb5d80c9b0cc9dd6c316b7c Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 18:00:07 +0800 Subject: [PATCH 161/166] Reword a test comment ruff read as a malformed noqa --- test/test_utils/test_no_blind_except.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_utils/test_no_blind_except.py b/test/test_utils/test_no_blind_except.py index a8216a24..c396c395 100644 --- a/test/test_utils/test_no_blind_except.py +++ b/test/test_utils/test_no_blind_except.py @@ -44,7 +44,7 @@ def test_every_blind_catch_re_raises_or_says_why(): def test_no_noqa_reason_has_a_comma(): - # SonarCloud reads what follows a comma in "# noqa: CODE — reason" as more + # SonarCloud reads what follows a comma in a noqa comment's reason as more # rule codes, and reports the comment as a malformed suppression (S7632) offenders = [] for path in sorted(PACKAGE.rglob("*.py")): From d73798630f363f124330625489a4a1515e93e3ec Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 18:02:05 +0800 Subject: [PATCH 162/166] Type the SSH widgets' optional login widget as optional --- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- .../pybreeze_ui/connect_gui/ssh/ssh_command_widget.py | 2 +- .../pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 50186038..d7f2e57d 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -910,3 +910,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: the thirteen `pybreeze/pybreeze_ui/tools_gui/*_gui.py`, `extend_ai_gui/code_review/cot_code_review_gui.py`, `extend_ai_gui/skills/skills_send_gui.py`, `jupyter_lab_gui/jupyter_lab_widget.py`, their tests, `test/test_utils/test_no_qt_member_shadowing.py` (new), `CLAUDE.md`, `architecture_explore.md` (test count) - **Checks**: the tool tab, AI panel, JupyterLab and Tools-menu tests pass (279); the new test passes, and the same check on the code before the rename listed the sixteen. `test/test_utils` collects 2712 tests in 150 files. - **Open items**: none. + +## U-20260925-109 · 2026-09-25 · The SSH widgets' optional login widget is typed as optional · #types #ssh + +- **What**: from the same mypy run as U-20260925-108: `SSHCommandWidget` and `SSHFileTreeManager` take `external_login_widget: LoginWidget = None`, a hint that says a login widget is required while `None` (build its own) is the default. +- **Change**: `LoginWidget | None = None` in both. +- **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py` +- **Checks**: ruff passes; the SSH re-entrancy and teardown tests pass (43). +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index 7fc10ec0..ac20c047 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-109 | 2026-09-25 | The SSH widgets' optional login widget is typed as optional | #types #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-108 | 2026-09-25 | No tool tab or panel hides a Qt method behind an attribute of the same name | #refactor #quality | [2026-09-c](2026-09-c.md) | | U-20260925-107 | 2026-09-25 | Opening the main window takes about 1 s instead of 3: the theme is applied once | #perf #startup | [2026-09-c](2026-09-c.md) | | U-20260925-106 | 2026-09-25 | The packaged build's regex worker: its timeout and its errors are tested | #test #regex | [2026-09-c](2026-09-c.md) | @@ -511,4 +512,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 108 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 109 | diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 8bd6a265..40921855 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -236,7 +236,7 @@ class SSHCommandWidget(QWidget): # Emitted when the session comes up or goes down, for the tab's status label state_changed = Signal() - def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: bool = True): + def __init__(self, external_login_widget: LoginWidget | None = None, add_login_widget: bool = True): super().__init__() self.word_dict = language_wrapper.language_word_dict self.setWindowTitle( diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index c6753fdf..3bf8283f 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -106,7 +106,7 @@ class SSHFileTreeManager(QWidget): # Emitted when the session comes up or goes down, for the tab's status label state_changed = Signal() - def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: bool = True): + def __init__(self, external_login_widget: LoginWidget | None = None, add_login_widget: bool = True): super().__init__() self.word_dict = language_wrapper.language_word_dict self.setWindowTitle( From 2f2b3a401b8a264219a7ec7a6b0e05dcc0d34251 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 18:06:32 +0800 Subject: [PATCH 163/166] Test runs from a Chinese folder with Chinese output --- architecture_explore.md | 2 +- docs/updates/2026-09-c.md | 8 ++++++ docs/updates/README.md | 3 +- test/test_utils/test_run_output.py | 46 ++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/architecture_explore.md b/architecture_explore.md index f9785d2b..de09c276 100644 --- a/architecture_explore.md +++ b/architecture_explore.md @@ -462,7 +462,7 @@ first_summary → first_code_review → judge_single_review ┐(評分前一 ## 18. 測試與 CI -- **單元測試** `test/test_utils/` — 150 個 `test_*.py`、2712 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 +- **單元測試** `test/test_utils/` — 150 個 `test_*.py`、2714 個測試(14 個 prthinker 契約測試在沒有 prthinker 的直譯器上跳過)。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定、JEditor 內部介面契約(`test_jeditor_contract.py`)、`except Exception` 只能重拋或註明理由(`test_no_blind_except.py`)等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。 - **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab - **CI** `.github/workflows/{dev,stable}.yml` — `unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布 - **覆蓋率** `.coveragerc` — `relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。`patch = subprocess` 也是必要的:pytest-cov 7 不再量測子行程,沒有它,測試在子直譯器裡建出的真主視窗(`started_window.py`)一行都不算。目前整體語句 96%、連分支 95%(`tools_gui` 99%、`utils/` 98%、`dialog` 100%;`menu` 97%、`connect_gui` 96%、`diagram_editor` 96%、`extend_ai_gui` 95%、`extend/` 94%、`jupyter_lab_gui` 92%;最低的是 `editor_main` 90%)。coverage 只追蹤 Python 自己開的執行緒,`test/test_utils/conftest.py` 讓每個 `QThread` 子類別的 `run` 在 Qt 的執行緒上裝上 coverage 的 tracer,否則沒有一個 `QThread.run` 算得到 diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index d7f2e57d..7c065b8b 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -918,3 +918,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py`, `pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py` - **Checks**: ruff passes; the SSH re-entrancy and teardown tests pass (43). - **Open items**: none. + +## U-20260925-110 · 2026-09-25 · Runs from a Chinese folder with Chinese output are tested end to end · #test #encoding + +- **What**: no test ran a real child from a folder with a non-ASCII name, or with output outside ASCII, through the two run paths (an automation package's `TaskProcessManager`, Run with...'s `FileRunnerProcess`); many users here have Chinese in their paths. Checked on the way: the IDE starts (the startup test exits 0) from a working folder named `中文路徑/專案` with the user folder under `中文路徑/家目錄`, Qt reports only the offscreen platform's own warnings as the IDE starts and every Tools tab opens and closes, and no Python warning (encoding ones included, `-X warn_default_encoding`) comes from PyBreeze's code. +- **Change**: `TestTextInAnyLanguage` in `test_run_output.py`: `python -m json.tool --no-ensure-ascii` on `中文資料夾/資料.json`, and a Run with... run of `中文資料夾/腳本.py` printing to stdout and stderr, both with `你好,世界 🙂 naïve` (a character outside the BMP and a Latin accent among them); the run window shows each line as written and the exit line. +- **Files**: `test/test_utils/test_run_output.py`, `architecture_explore.md` (test count) +- **Checks**: the two tests pass as the code stands. `test/test_utils` collects 2714 tests in 150 files. +- **Open items**: none. diff --git a/docs/updates/README.md b/docs/updates/README.md index ac20c047..0e582346 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-110 | 2026-09-25 | Runs from a Chinese folder with Chinese output are tested end to end | #test #encoding | [2026-09-c](2026-09-c.md) | | U-20260925-109 | 2026-09-25 | The SSH widgets' optional login widget is typed as optional | #types #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-108 | 2026-09-25 | No tool tab or panel hides a Qt method behind an attribute of the same name | #refactor #quality | [2026-09-c](2026-09-c.md) | | U-20260925-107 | 2026-09-25 | Opening the main window takes about 1 s instead of 3: the theme is applied once | #perf #startup | [2026-09-c](2026-09-c.md) | @@ -512,4 +513,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 109 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 110 | diff --git a/test/test_utils/test_run_output.py b/test/test_utils/test_run_output.py index c70f399f..8177958d 100644 --- a/test/test_utils/test_run_output.py +++ b/test/test_utils/test_run_output.py @@ -624,3 +624,49 @@ def test_stop_ends_what_the_run_started_too(qt_app, tmp_path): finally: _stop_grandchild(run_window.code_result.toPlainText()) + + +class TestTextInAnyLanguage: + """A folder and output in Chinese, and a character outside the BMP, reach the run window intact.""" + + _TEXT = "你好,世界 🙂 naïve" + + def test_a_package_run(self, qt_app, tmp_path): + from pybreeze.extend.process_executor.process_executor_utils import build_task_process + + folder = tmp_path / "中文資料夾" + folder.mkdir() + data = folder / "資料.json" + data.write_text(json.dumps({"問候": self._TEXT}, ensure_ascii=False), encoding="utf-8") + process = build_task_process(MainWindow(sys.executable)) + + process.start_module_process("json.tool", ["--no-ensure-ascii", str(data)]) + _run_events_until(qt_app, lambda: process.process is None) + + text = process.main_window.code_result.toPlainText() + assert f'"問候": "{self._TEXT}"' in text + assert text.endswith("Task exit with code 0\n") + + def test_a_run_with_run(self, qt_app, tmp_path): + from pybreeze.extend.process_executor.file_runner_process import FileRunnerProcess + from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow + + folder = tmp_path / "中文資料夾" + folder.mkdir() + script = folder / "腳本.py" + script.write_text( + "import sys\n" + f"print({self._TEXT!r})\n" + f"print('錯誤:' + {self._TEXT!r}, file=sys.stderr)\n", + encoding="utf-8", + ) + window = CodeWindow() + runner = FileRunnerProcess(window) + + runner.run_file({"name": "Python", "compiler": sys.executable}, str(script)) + _run_events_until(qt_app, lambda: runner.process is None) + + text = window.code_result.toPlainText() + assert f"{self._TEXT}\n" in text + assert f"錯誤:{self._TEXT}\n" in text + assert text.endswith("[Process exited with code 0]\n") From ebf0a7cf258ece5c01196d493fa110207815a00f Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 18:11:44 +0800 Subject: [PATCH 164/166] Record what Python 3.15 needs from PySide6; badge the supported range --- README.md | 2 +- README/README_zh-CN.md | 2 +- README/README_zh-TW.md | 2 +- docs/updates/2026-09-c.md | 8 ++++++++ docs/updates/README.md | 3 ++- progress.md | 1 + 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec605954..6836487e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PyBreeze: The Automation-First IDE -[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) +[![Python 3.10–3.14](https://img.shields.io/badge/python-3.10--3.14-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![PySide6](https://img.shields.io/badge/GUI-PySide6-green.svg)](https://doc.qt.io/qtforpython/) [![Documentation](https://readthedocs.org/projects/pybreeze/badge/?version=latest)](https://pybreeze.readthedocs.io/en/latest/index.html) diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 0b0d893c..769d395d 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -1,6 +1,6 @@ # PyBreeze:自动化优先的 IDE -[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) +[![Python 3.10–3.14](https://img.shields.io/badge/python-3.10--3.14-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE) [![PySide6](https://img.shields.io/badge/GUI-PySide6-green.svg)](https://doc.qt.io/qtforpython/) [![Documentation](https://readthedocs.org/projects/pybreeze/badge/?version=latest)](https://pybreeze.readthedocs.io/en/latest/index.html) diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 6845da54..318c2972 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -1,6 +1,6 @@ # PyBreeze:自動化優先的 IDE -[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) +[![Python 3.10–3.14](https://img.shields.io/badge/python-3.10--3.14-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE) [![PySide6](https://img.shields.io/badge/GUI-PySide6-green.svg)](https://doc.qt.io/qtforpython/) [![Documentation](https://readthedocs.org/projects/pybreeze/badge/?version=latest)](https://pybreeze.readthedocs.io/en/latest/index.html) diff --git a/docs/updates/2026-09-c.md b/docs/updates/2026-09-c.md index 7c065b8b..178ca052 100644 --- a/docs/updates/2026-09-c.md +++ b/docs/updates/2026-09-c.md @@ -926,3 +926,11 @@ Continues [2026-09-b.md](2026-09-b.md), which passed the batch size limit. Index - **Files**: `test/test_utils/test_run_output.py`, `architecture_explore.md` (test count) - **Checks**: the two tests pass as the code stands. `test/test_utils` collects 2714 tests in 150 files. - **Open items**: none. + +## U-20260925-111 · 2026-09-25 · Python 3.15 checked: PyBreeze cannot be installed there until PySide6 moves; the badge says 3.10–3.14 · #compat #docs + +- **What**: Python 3.15 is due in October 2026 (3.15.0rc2 is out) and CI runs 3.10–3.14. In a 3.15.0rc2 venv (python.org's NuGet build; uv's list stopped at 3.15.0a8) `pip install -r requirements.txt` stops at `PySide6==6.11.2`: every PySide6 6.11 wheel says `Requires-Python <3.15`. PySide's `dev` branch raised the bound to `<3.16` on 2026-08-25 ("Bump Python supported version to 3.15"), and on 2026-08-28 moved QtWebEngine out of `PySide6` into a `PySide6_WebEngine` wheel ("Add new wheel for WebEngine"; `PySide6` becomes Essentials and Addons, and QtPdf gets a wheel too). Neither that release nor `PySide6_WebEngine` is on PyPI yet; je-editor 1.0.28 and frontengine 1.0.81 pin `PySide6==6.11.2` too. The whole source tree compiles on 3.15.0a8 without a warning. The README's badge said Python 3.10+ while its requirements said 3.10 – 3.14. +- **Change**: `progress.md` #121 records what moving to 3.15 takes (the PySide6 release after 6.11, with `PySide6_WebEngine` beside it in `requirements.txt`, `pyproject.toml` and `dev.toml`, since JEditor imports `QtWebEngineWidgets` as the IDE starts and the JupyterLab tab uses a `QWebEngineView`; je-editor and frontengine moving with it; then the CI matrix, classifiers and README) and what blocks it. The badge in `README.md`, `README/README_zh-TW.md` and `README/README_zh-CN.md` reads 3.10–3.14. +- **Files**: `progress.md` (#121), the three READMEs +- **Checks**: shields.io renders the new badge as "python: 3.10-3.14" (HTTP 200). +- **Open items**: `progress.md` #121. diff --git a/docs/updates/README.md b/docs/updates/README.md index 0e582346..38842b1c 100644 --- a/docs/updates/README.md +++ b/docs/updates/README.md @@ -58,6 +58,7 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here | ID | Date | Title | Tags | Batch | |---|---|---|---|---| +| U-20260925-111 | 2026-09-25 | Python 3.15 checked: PyBreeze cannot be installed there until PySide6 moves; the badge says 3.10–3.14 | #compat #docs | [2026-09-c](2026-09-c.md) | | U-20260925-110 | 2026-09-25 | Runs from a Chinese folder with Chinese output are tested end to end | #test #encoding | [2026-09-c](2026-09-c.md) | | U-20260925-109 | 2026-09-25 | The SSH widgets' optional login widget is typed as optional | #types #ssh | [2026-09-c](2026-09-c.md) | | U-20260925-108 | 2026-09-25 | No tool tab or panel hides a Qt method behind an attribute of the same name | #refactor #quality | [2026-09-c](2026-09-c.md) | @@ -513,4 +514,4 @@ In the same commit: delete the item from `progress.md`, add a `#done` entry here |---|---|---:| | [2026-09.md](2026-09.md) | 2026-09 | 218 | | [2026-09-b.md](2026-09-b.md) | 2026-09 | 120 | -| [2026-09-c.md](2026-09-c.md) | 2026-09 | 110 | +| [2026-09-c.md](2026-09-c.md) | 2026-09 | 111 | diff --git a/progress.md b/progress.md index 3e645482..4a07122e 100644 --- a/progress.md +++ b/progress.md @@ -29,3 +29,4 @@ Cross-repo and workspace items live in `D:\Codes\progress.md` (relevant here: X- - **#118** [BLOCKED] A settings file whose `ui_style` is not a string stops the IDE from starting: `EditorMain.__init__` calls `startup_setting()` unguarded (`je_editor/pyside_ui/main_ui/main_editor.py:245`), which hands the value to qt_material's `apply_stylesheet`, and a number raises `TypeError` inside the window's constructor, before PyBreeze can do anything (reproduced on je_editor 1.0.27 with `{"ui_style": 42}` in `.jeditor/user_setting.json`: the window was never built). A theme name that does not exist only logs a warning. Needs JEditor to check the saved style's type, or to guard that call as `_restore_open_files_session` is guarded. - **#119** [BLOCKED] Most of the IDE's start is JEditor importing two tools nobody has opened yet: `build_dock_menu.py:17,22` and `build_tab_tools_menu.py:9-10` (`je_editor/pyside_ui/main_ui/menu/`) import `ChatUI`, which brings `langchain_openai` and `openai` (2.0–2.5 s), and `IpythonWidget`, which brings IPython (about 1 s), at the top. With `-X importtime` on je_editor 1.0.27, `build_dock_menu` is 3.1–3.7 s of the 4.1–4.9 s it takes to import PyBreeze's main-window module. PyBreeze imports its own heavy GUIs (the automation packages', SSH) when they are first opened. Needs JEditor to import these two in the actions that open them. - **#120** [BLOCKED] A locust script run with JEditor's Run Program, Run Debugger or shell runs its HttpUser users one at a time: those start their process with the IDE's own environment (`subprocess.Popen` with no `env`, `je_editor/pyside_ui/code/code_process/code_exec.py:99,194`, `.../shell_process/shell_exec.py:95`), which carries `LOCUST_SKIP_MONKEY_PATCH` (PyBreeze sets it so that locust cannot patch the IDE with gevent), and unpatched a 3 s test of 10 users took over three minutes. PyBreeze's own runs (the automation menus, Run with..., installs, the JupyterLab server) leave it out through `child_environment()`, which drops every variable whose value is `subprocess_util.IDE_ONLY`. Needs JEditor to let its host give the environment for the processes it starts, or to drop variables with that value. +- **#121** [BLOCKED] PyBreeze cannot be installed on Python 3.15 (due October 2026): `requirements.txt`, `pyproject.toml` and `dev.toml` pin `PySide6==6.11.2`, whose wheels say `Requires-Python <3.15` (with 3.15.0rc2, pip finds no PySide6 to install). PySide's `dev` branch raised the bound to `<3.16` on 2026-08-25 for its next release, and three days later moved QtWebEngine out of `PySide6` into a wheel of its own, `PySide6_WebEngine` (`PySide6` is then Essentials and Addons only; `create_wheels.py`). Moving the pin to that release therefore also needs `PySide6_WebEngine` beside it, in all three files: JEditor imports `QtWebEngineWidgets` as the IDE starts (its browser) and the JupyterLab tab is a `QWebEngineView`. je-editor and frontengine pin `PySide6==6.11.2` as well and move with it (workspace). Then add 3.15 to the CI matrix, the classifiers and the README's version range. Blocked until that PySide6 release, and `PySide6_WebEngine`, are on PyPI. From f79f813e079982e5ab76686be2367b3ff95a0cca Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 18:18:10 +0800 Subject: [PATCH 165/166] Clear the release PR's static-analysis findings: passphrase labels are UI text, the PKCS#8 header is a format marker, the main window reference is released explicitly after exec --- pybreeze/extend_multi_language/extend_english.py | 6 +++--- .../extend_multi_language/extend_traditional_chinese.py | 4 ++-- pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py | 2 +- pybreeze/pybreeze_ui/editor_main/main_ui.py | 3 ++- pybreeze/utils/curl_import/request_body.py | 3 ++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 891edaf2..fe707529 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -177,7 +177,7 @@ "ssh_command_widget_dialog_title_key_error": "Key error", "ssh_command_widget_dialog_message_key_file_not_exist": "Key file does not exist.", "ssh_command_widget_error_message_unsupported_private_key": "Unsupported or invalid private key.", - "ssh_key_error_passphrase_needed": "The private key is protected by a passphrase: enter it in the Passphrase field.", + "ssh_key_error_passphrase_needed": "The private key is protected by a passphrase: enter it in the Passphrase field.", # nosec B105 # NOSONAR S2068 — UI text "ssh_key_error_passphrase_wrong": "The passphrase for the private key is wrong.", "ssh_key_error_putty_key": "This is a PuTTY key (.ppk), which cannot be used here: load it in PuTTYgen, " "choose Conversions > Export OpenSSH key, and pick the exported file.", @@ -275,11 +275,11 @@ "ssh_login_widget_label_user": "User", "ssh_login_widget_label_key": "Key", "ssh_login_widget_label_password": "Password", - "ssh_login_widget_label_passphrase": "Passphrase", + "ssh_login_widget_label_passphrase": "Passphrase", # nosec B105 # NOSONAR S2068 — UI text "ssh_login_widget_placeholder_host": "Host name or IP address", "ssh_login_widget_placeholder_username": "Username", "ssh_login_widget_placeholder_password": "Password", - "ssh_login_widget_placeholder_passphrase": "The key's passphrase, if it has one", + "ssh_login_widget_placeholder_passphrase": "The key's passphrase, if it has one", # nosec B105 # NOSONAR S2068 — UI text "ssh_login_widget_placeholder_private_key": "Private key path (OpenSSH or PEM)", "ssh_login_widget_button_use_key_auth": "Use key auth", "ssh_login_widget_button_connect": "Connect", diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index dc586d5f..c9049ca5 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -265,11 +265,11 @@ "ssh_login_widget_label_user": "使用者", "ssh_login_widget_label_key": "金鑰", "ssh_login_widget_label_password": "密碼", - "ssh_login_widget_label_passphrase": "密語", + "ssh_login_widget_label_passphrase": "密語", # nosec B105 # NOSONAR S2068 — UI text "ssh_login_widget_placeholder_host": "主機名稱或 IP 位址", "ssh_login_widget_placeholder_username": "使用者名稱", "ssh_login_widget_placeholder_password": "密碼", - "ssh_login_widget_placeholder_passphrase": "私鑰的密語(沒有就留空)", + "ssh_login_widget_placeholder_passphrase": "私鑰的密語(沒有就留空)", # nosec B105 # NOSONAR S2068 — UI text "ssh_login_widget_placeholder_private_key": "私鑰路徑(OpenSSH 或 PEM)", "ssh_login_widget_button_use_key_auth": "使用金鑰驗證", "ssh_login_widget_button_connect": "連線", diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py index 9b1dc5f3..7c739c1b 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_key_loader.py @@ -33,7 +33,7 @@ _PUTTY_HEADER = b"PuTTY-User-Key-File-" # PKCS#8, which paramiko does not read (openssl genpkey, ssh-keygen -m PKCS8) -_PKCS8_PLAIN = b"-----BEGIN PRIVATE KEY-----" +_PKCS8_PLAIN = b"-----BEGIN PRIVATE KEY-----" # nosemgrep # gitleaks:allow — a format marker, not a key _PKCS8_ENCRYPTED = b"-----BEGIN ENCRYPTED PRIVATE KEY-----" diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index 544eccc7..9d099d4a 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -228,8 +228,9 @@ def start_editor(debug_mode: bool = False, theme: str | None = None, **kwargs) - # Qt objects on the worker and crashed the IDE later collect_garbage_on_gui_thread(new_ide) # Held until the application ends: the window is nobody else's - _window = open_main_window(new_ide, debug_mode=debug_mode, theme=theme, **kwargs) + window = open_main_window(new_ide, debug_mode=debug_mode, theme=theme, **kwargs) ret = new_ide.exec() + del window os._exit(ret) diff --git a/pybreeze/utils/curl_import/request_body.py b/pybreeze/utils/curl_import/request_body.py index ce52e8a0..b7289e1f 100644 --- a/pybreeze/utils/curl_import/request_body.py +++ b/pybreeze/utils/curl_import/request_body.py @@ -78,7 +78,8 @@ def _exact_float(text: str) -> float: survives = f"{text} does not survive as a float" if not math.isfinite(value): raise ValueError(survives) - if Decimal(repr(value)) != Decimal(text): + # Decimal compares by value: "1.5" round-trips and this is false + if Decimal(repr(value)) != Decimal(text): # NOSONAR S2583 raise ValueError(survives) return value From 0156ea86dae7d45d0a688b22d09d1d50eb9db530 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 25 Sep 2026 18:35:43 +0800 Subject: [PATCH 166/166] docs: CLAUDE.md: look up SonarCloud/Codacy findings through their APIs with the env keys; never reveal a key --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9742eab5..d01cbc6c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,3 +177,4 @@ Workspace rule shared by every repository under `D:\Codes` (full text: `D:\Codes - Commit messages: short imperative sentence ("Update stable version", "Fix github actions") - **No AI attribution (mandatory)** — never mention any AI tool, assistant, agent, model or vendor in commit messages, trailers, branch names, PR titles or bodies, issues, code comments or documentation. No `Co-Authored-By` referencing an AI, no "Generated with …" footers. PR text describes *what changed and why*, never how it was authored. - PR target: `dev` for development work, `main` for stable releases +- **SonarCloud / Codacy findings.** When a PR or commit fails a SonarCloud or Codacy check, look the findings up through their APIs instead of guessing. The keys are in environment variables: `SonarCloudToken` (SonarCloud, e.g. `curl -s -u "$SonarCloudToken:" "https://sonarcloud.io/api/issues/search?componentKeys=&pullRequest=&resolved=false"`) and `CODACY_PROJECT_TOKEN` (a Codacy project token, valid only for its own project: any other repository answers "Bad credentials", so for a public repository query `https://app.codacy.com/api/v3/analysis/organizations/gh//repositories//pull-requests//issues?status=new` without a key). **Never reveal a key or any personal credential while doing so**: refer to the variables by name only, never echo or print their values, and never put them in files, commit messages, PR or issue text, logs, or any output that leaves the machine.

    aQE%Y$y#tU0jXDr`{BSN;c61G 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