@@ -54,22 +54,70 @@ an install hint (`editor.py` import guard) — `prview` imports GtkSource
5454
5555## Behaviours
5656
57- ** Opening** (` open_file(path, restore_cursor) ` ): guarded by
58- ` editorfiles.load_guard ` (size, binary, image) and ` is_inside(root) ` ;
59- language from ` guess_language_id ` (extension, then the first line's shebang;
60- its sibling ` fence_language_id ` maps a markdown fence's info word — ` python `
61- → ` python3 ` , ` bash ` → ` sh ` — for the PR page's code blocks in ` mdwidgets ` );
62- style scheme from ` editor.style_scheme(setting, dark) ` (a bare
63- ` GtkSource.Buffer ` defaults to the light ` classic ` scheme — never leave it
64- unset). Cursor placement on a fresh buffer must re-issue ` scroll_to_mark ` from
65- a ` PRIORITY_LOW ` idle (` _apply_cursor ` ): line heights are estimates until
57+ ** Opening** (` open_file(path, restore_cursor) ` ): guarded by ` is_inside(root) `
58+ and ` editorfiles.is_image_path ` ; everything else about the file is the
59+ service's (below). Language from ` guess_language_id ` (extension, then the
60+ first line's shebang — ` editorfiles.first_line ` of the text the service
61+ read; its sibling ` fence_language_id ` maps a markdown fence's info word —
62+ ` python ` → ` python3 ` , ` bash ` → ` sh ` — for the PR page's code blocks in
63+ ` mdwidgets ` ), switched off above 512 KiB (` should_highlight(size) ` ); style
64+ scheme from ` editor.style_scheme(setting, dark) ` (a bare ` GtkSource.Buffer `
65+ defaults to the light ` classic ` scheme — never leave it unset). Cursor
66+ placement on a fresh buffer must re-issue ` scroll_to_mark ` from a
67+ ` PRIORITY_LOW ` idle (` _apply_cursor ` ): line heights are estimates until
6668validation idles run, so an immediate scroll lands ~ line 44 for a target of
6769602 . ` open_in_editor ` (the MCP tool, the terminal's Ctrl+click on a path,
6870"Add to chat" in reverse) all land in ` MainWindow.open_in_tab_editor ` .
6971
70- ** External changes** : each open file has a ` Gio.FileMonitor ` ; a clean buffer
71- reloads silently, a dirty one is told. The agent rewrites these files
72- constantly, so this path is exercised more than manual saves are.
72+ ** The files are the service's** (split-service spec §3.23, PR-2.3). The
73+ pane opens no file and writes none: ` GtkSource.File ` , ` FileLoader ` and
74+ ` FileSaver ` are gone, and every path is a path on the service's machine.
75+ ` collins/remotefiles.py ` (GTK-free) is the client's end, `collins/service/
76+ files.py` the service's:
77+
78+ - ` _start_load ` asks ` fs.read ` from a worker thread (` _off_main ` : a daemon
79+ thread, the answer landed at ` GLib.PRIORITY_DEFAULT ` ) and ` _on_loaded `
80+ fills the buffer outside the undo history (` _fill ` ), keeping the reply's
81+ ` mtime ` and ` encoding ` on the ` _OpenFile ` . The guards moved with the
82+ read: the service refuses a path that is not a regular file, one over
83+ 5 MiB (` protocol.FILE_TEXT_MAX ` ) and one outside every root it knows
84+ (` files.allowed ` : a live session's cwd, a store session's cwd or project
85+ root, anything for a ` local ` client), and flags ` binary ` (a NUL in the
86+ first 8 KiB) and ` latin-1 ` (bytes that are not UTF-8, written back the
87+ same way). A refusal's words come translated through
88+ ` remotefiles.refusal_words ` into the banner; a fresh open closes its tab,
89+ a reload keeps it. ` load_id ` still makes a superseded read (a rename's)
90+ a no-op.
91+ - ` _do_save ` sends ` fs.write ` with the buffer's text and ` expect_mtime ` :
92+ Ctrl+S (` _save ` ) expects the mtime of the last read or write, and a file
93+ that moved underneath is refused ` stale ` by the service with ** nothing
94+ written** — ` _on_saved ` raises the "changed on disk" dialog, whose
95+ Overwrite saves again with ` expect_mtime ` null. ` save_all ` and the close
96+ flows' Save pass null from the start (the user's explicit consent, as
97+ before). The service writes by a temp file in the directory and one
98+ ` os.replace ` , keeps the mode and follows a symlink to the file.
99+ - ` _watch_external_changes ` installs ` fs.watch kind: file ` under a handle
100+ the client mints (` remotefiles.Watcher ` ; ` _teardown_page ` unwatches);
101+ the service's ` Gio.FileMonitor ` debounces 300 ms, stats on a thread and
102+ pushes ` file-changed {handle, path, mtime, size, gone} ` once per burst
103+ whose stat moved. ` _check_external ` judges it against ` opened.mtime ` : a
104+ clean buffer reloads silently (cursor kept), a dirty one is told
105+ (Reload), ` gone ` marks the buffer dirty and says so. An event that
106+ arrives while a save or load is in flight waits (` pending_change ` ) for
107+ the reply's mtime, so the editor's own write never reads as a change.
108+ The link is installed behind the ` files ` capability of the service's
109+ hello (` App._install_file_transport ` , decided on every connect like
110+ git's); against a service without it nothing is sent and every open
111+ lands in the banner. A reconnect (` remotefiles.reset() ` in
112+ ` App._on_connected ` ) re-sends every live watch. The agent rewrites these files constantly, so this path is
113+ exercised more than manual saves are.
114+ - A text over the 1 MiB frame cap crosses as ` TAG_BLOB ` chunks either way
115+ (` protocol.split_request ` / ` split_reply ` , the ` text_chunked ` /
116+ ` text_bytes ` fields), so a 5 MiB file saves.
117+ - ` check_editor_save.py ` drives all of it against a scratch service through
118+ ` e2e_service.harness_link() ` plus ` remotefiles.install(link) ` ; a widget
119+ check that opens files needs the same two lines, or every open lands in
120+ the banner with "Not connected to the service".
73121
74122** Following the session** (` request_root ` / ` offer_root ` ): the tab's cwd tick
75123calls ` _maybe_follow_editor ` ; ` editorfiles.follow_scope(root, cwd) ` and
@@ -137,4 +185,6 @@ close state joins all three.
137185 the missing-typelib exit.
138186
139187Related: ` collins-terminal-tab ` , ` collins-panel-dock ` ,
140- ` collins-gtk-sharp-edges ` , ` collins-testing ` (` check_editor_narrow.py ` ).
188+ ` collins-gtk-sharp-edges ` , ` collins-testing ` (` check_editor_narrow.py ` ,
189+ ` check_editor_save.py ` ), ` collins-session-mcp-tools ` (the API's message
190+ table in ` api/protocol.py ` : the ` fs.* ` types).
0 commit comments