editor implementation
Declared HTTP patterns
Core 0.30.4 first resolved the complete /projects/:id/... declarations before invoking src/api.ts; the plugin declares core 0.30.7 and shared API 14. req.path is empty; req.params.id and the commit-diff req.params.hash are decoded once by core. An undeclared suffix returns dispatcher 404 before Project lookup, Sandbox dispatch or filesystem access. Project membership, root selection and file confinement remain Editor-owned.
Project selection
UI API 62 supplies the exact host utils.projectName and components.ProjectIcon for the
administrator picker. The ordinary picker uses ProjectFilterPills, which composes that same pair.
Both receive the complete /projects projection, including computer.label and execution kind.
The computer slug remains an identifier; workspace status and icon-picker descriptions use the same
name helper. The system root retains its separate hard-drive option.
src/api.ts::projectFor awaits the host's current Project row before any root or filesystem work. A deleted or inaccessible Project returns the existing HTTP 404 response.
Editor requires UI API 62. Project members use the host ProjectFilterPills catalog adapter, which renders the shared single-choice list and accepts value, onChange and includeAll. The retired variant option is removed from the consumer. Picking a project retains the editor's existing root selection, remembered destination and dirty-draft confirmation. Administrators still use the authorized system selector. This presentation change adds no file requests or inference work; project icons use the host's existing root-aware image loader.
Paired computer Projects
projectFor resolves a local Project through Sandbox's resolveLocalTarget for the acting account.
It passes the resolved cwd and platform into the same managed/context.ts backend as managed Projects.
The context selects {kind:'local',projectId}, uses shared localTargetPath for native POSIX or Windows
path resolution and returns slash-separated relative tree names to the browser. Editor requests remain
confined to the selected root. Download headers use the native basename without the computer's directory
path. Project means the connection's cwd; System means its filesystem root, including a Windows drive
or UNC share. These navigation roots do not confine the computer's OS user or add a second filesystem transport.
Every file operation calls the live sandbox.projectFiles({project,accountUserId,operation}) control.
Core holds and releases its daemon binding, checks live member-only canExecute and counts writes
through the device's existing file-edit accounting. Git and commit inspection call
editorExecute(ctx, projectRef, accountUserId, command, cwd), which uses the same
sandbox.runCommand contract and editor lease. The core Git overview uses localGitReader.
Uploads retain the host's shared resumable upload client and may name the computer Project without
a conversation; core pins their connection through commit or abort and keeps collision suffixes.
Stable computer refusals retain their HTTP code: local_target_forbidden 403,
local_target_unavailable 404, and LOCAL_TARGET_OFFLINE, LOCAL_EXECUTION_LOST 409. The browser receives curated wording, never provider diagnostics.
Missing Sandbox remains 503. An administrator who is not a member cannot inspect or modify files.
Editor 0.6.20 requires core 0.30.7; every paired CLI client serves its file and command paths.
The browser reads computer.cwd and computer.fileSystemRoot from the core Project projection
only for captions. It offers both roots and keeps System Git controls hidden. Office documents on
computers use the existing download view; no converter or shell-specific utility is launched.
No additional connection, background worker or inference request is introduced by the plugin.
Browser draft lifecycle
web-src/editor/useEditorDrafts.ts owns the selected file, open tabs, draft text, dirty paths, content versions, per-path save queues and discard confirmation for ProjectEditor. Callers key the editor by Project identity. A root change confirms unsaved work, then clears tabs, drafts, expanded folders and the selection and returns the view to Edit. The chosen root remains remembered per device. ProjectEditor fills the workspace height supplied by EditorPage.
fileData.ts::editorDirectoryQuery(projectId, root, directory) owns directory request options and keys beneath the Project/root prefix. The first tree level and ProjectIconPicker use that same host QueryClient cache. Opened levels in useLazyDirs reuse the directory request while retaining their generation-owned in-flight reads; after a mutation, a new generation cannot join a response started before that mutation. Tree mutations invalidate the root prefix, including the currently open icon directory; lazy levels retain their epoch/root reset and discard responses from an obsolete generation. The picker only reads when its managed environment is ready. Each read lists one directory and spends no inference calls; unopened directories do no work.
Each save captures Project, root, path and text and publishes through the existing root-aware fileData.ts query keys and host QueryClient. Writes to one path serialize even without a content version; different paths can proceed together. A queued write uses the previous response's version. Save completion retires only a draft that still matches the sent text, so typing during a pending save remains visible and dirty. Every operation reports its own result. Closing a dirty tab asks for confirmation; rename remaps tabs and drafts, while deletion forgets the affected path and descendants. Desktop fullscreen preserves the mounted draft state; mobile close still passes through the discard guard.
web-src/editor/menu.ts builds tree context menus and File, View and Settings descriptors. Desktop passes them to the host ContextMenu; phones flatten those same descriptors for ActionMenu and run the selected action after the menu closes. Actions receive the current root, selection, preferences and existing callbacks. A directory's context menu opens the file picker with that exact upload directory captured until selection completes. Empty selection keeps selection-dependent actions disabled; host Projects omit the managed root submenu, and System roots omit Git views.
Managed request ownership
The managed server dispatcher in src/managed.ts keeps the existing mount and method checks, linked-account and provider admission, unsupported-operation HTTP 501 and final error response. The API's authorized Project and root target enters src/managed/context.ts once per request. That context derives the guest root and binds confined paths, live provider resolution, shared-client reads and follow-stat, request input and refusal mapping. File and command operations resolve the live provider again rather than retaining the admission provider for execution.
Named handlers in src/managed/files.ts own listings, text saves, entry mutations, streamed downloads and Office previews, including the shared two-conversion limit. src/managed/git.ts owns the read-only history routes and rejects them outside the Project root. Every handler receives the same authorized context; these modules add no backend registration or public core API. The dispatcher awaits handlers inside its error boundary, so validation, upload and access refusals retain their existing status and wording while unknown failures remain sanitized HTTP 503. A guest already_exists on a write answers HTTP 409 with already exists; rename and copy answer HTTP 409 with target already exists. The provider's diagnostic and internal paths are not forwarded. Existing-directory mkdir remains idempotent.
Managed command and browser contracts
Editor and OneDrive execute one-shot project commands with sandbox.runCommand(input, actor, options). The host authorizes the prepared target and owns byte capture, heartbeat, timeout, cancellation and lease release; the actor preserves the caller's account and allowed roots. Editor uses leaseKind: 'editor', a 30-second timeout and an 8 MiB combined output bound, returns sanitized stdout, and rejects nonzero results. OneDrive uses leaseKind: 'files', its Git timeout of 120 seconds by default, and a 64 MiB combined bound. It retains real Git exit codes and maps interrupted execution to code: null with sanitized partial output; cleanup and authorization errors remain exceptions. Missing Sandbox support makes the operation unavailable. Long-lived services retain raw prepareExecution sessions. No consumer timer or second capture loop is required.
tests/p65ManagedConsumers.test.ts supplies both the exit verdict and stdout/stderr EOF for completed prepared sessions. The host runner must drain both streams before returning or releasing the lease. Pending, interrupted and rejected sessions retain their distinct cancellation and partial-output paths; the fixture does not replace them with successful closure.
The binary preview and status bar read the viewer locale from the existing host translation hook and pass it to the published host byte formatter; switching languages changes numeric captions without a local formatter.
Editor JSON writes call runtime.api(path, { method, json: payload }) through ElowenUiRuntime['api']; the host owns serialization, headers, authentication and request lifecycle. File saves omit an undefined version and preserve an explicit null version. Omitting json retains raw request behavior, including downloads and multipart uploads. Consumers still validate their domain responses.
Host file listings
The authorized GET /projects/:id/files handler awaits listProjectFiles(root, maxDepth, from) inside its existing error boundary. The function returns a promise of the same flat FileNode[]. Directory reads, target stats and realpath checks use asynchronous filesystem operations so a large host tree does not occupy the daemon event loop until completion. Traversal remains sequential and depth-first, with directories sorted before other directory entries and names ordered by the existing locale comparison. The depth budget, ignored names, staging prefix, system virtual-filesystem exclusions, symlink containment and visited-realpath set are unchanged. Missing roots return an empty tree; unreadable directories and entries retain their existing skip behavior.
The caller still authorizes the Project or administrator System root and guards from before listing. Directory symlinks are shown but followed only within the real root, once per real directory. Managed listings continue through the existing account-bound Sandbox transport. No background task, listing cache or parallel read pool is introduced. Async filesystem round trips can increase total listing time while allowing unrelated daemon requests to run between operations. tests/hostFileWalks.test.mjs checks ordering, depth and event-loop progress; the existing Project, System and managed route suites retain authorization coverage.
File writes
Host mutation request parsing uses isRecord from
elowen-plugin-shared/record after the existing JSON parser. It accepts
non-null, non-array objects without imposing a prototype requirement.
Malformed JSON, null, scalars and arrays retain the parser's null result;
each route retains its required-field refusal and performs no file write
when those fields are absent. Empty objects pass the basic guard but not
required-field validation. Project authorization and managed dispatch are
unchanged; the guard has no I/O or persistence policy.
Browser uploads call runtime().utils.uploadProjectFile(file, { projectId, directory }, { signal, onProgress }), published by UI API 56. The implementation lives in the host's web/lib/projectUploads.ts, also called by chat, so the plugin bundles no upload transport. Core opens an account-bound session, accepts sequential Blob parts at durable offsets, retries resumable failures and commits the verified size. There is no type or total size limit. Existing destinations and in-flight managed upload reservations gain a collision suffix rather than being replaced.
ProjectEditor renders each pending file with the host Progress component and cancel button. Commit refreshes the root listing and opened directories. Cancellation, unmount or changing Project/root aborts pending work; a failure shows the core refusal and leaves other files queued. Upload is offered only in the Project view: the core target directory must remain inside that Project, and System browsing/editing retains its existing authorization.
The old PUT /projects/:id/upload, editor session map, managed helper and browser client are removed. Core rechecks current Project membership and destination identity at every part and commit, confines host symlink paths and aborts guest handles resolved outside the Project before sending bytes. Host listings hide the reserved .elowen-upload- staging prefix. Sandbox guest list and walk own managed staging visibility: they omit .elowen-upload-, .elowen-write- and .elowen-copy- before producing a page, including symlinks, and never descend into staging directories. Editor consumes those results without a staging filter under Project and System roots, including linked directory expansion. It retains repository ignores and System virtual-path rules. Sandbox also owns staging cleanup. Host staging adds its prefix after filename sanitization. A daemon restart loses in-memory sessions; core's existing boot/daily attachment worker reclaims host staging after an hour without writes, while Sandbox owns guest staging expiry. Ordinary .part files remain visible and are not swept.
All guest file I/O uses elowen-plugin-shared/guestFiles with the editor's authorized projectFiles binding. writeGuestFile(files, path, bytes, { expectedVersion }) selects a single write up to 512 KiB and chunked upload above it, creating parents only after parent_missing. The client also owns stat, metadata-only listing of every page, bounded walk, mkdir, remove, rename and copy. The plugin retains root confinement, listing filters, request validation and HTTP refusal mapping; it constructs no guest operation literals. Tree duplication calls copyGuestFile(files, from, to, { expectedVersion }) after statting the selected source. The guest preserves symlinks and refuses an existing destination; only parent_missing asks the client to create the target parent and repeat the same source CAS copy. Deletion calls removeGuestFile(files, path, { expectedVersion, recursive: source.kind === 'directory' }). Directory CAS pins the selected entry's existing version, not a whole-tree snapshot. Both mutations retain the authorized root and HTTP guards and start no commands. Git and Office conversion retain their command workloads. Without Sandbox, the request is unavailable.
File downloads
web-src/editor/fileUrls.ts::downloadEditorFile(projectId, root, path) is the shared download action for ProjectEditor menus and BinaryPreview. It builds the root-aware raw URL with download=1, names the attachment with the existing basename helper, clicks a temporary anchor and removes it. Authentication stays with the same-origin cookie proxy; the action buffers no bytes and creates no object URL. With no download action, it performs no work.
The existing authorized /projects/:id/raw route returns a streamed PluginHttpResponse.body; download=1 adds the attachment header. Project selection and administrator-only host System authorization are unchanged. Binary files, and image, PDF, Office and media files above their preview limits, show an enabled Download button instead of reading their content into the browser. An oversized text file shows a notice and no download button.
Host Project and System downloads use their existing path guard, re-authorize the resolved leaf and open it with O_NOFOLLOW, and take size metadata from that same descriptor. A Node read stream owns the descriptor and reads in 64 KiB blocks; EOF, errors and core-managed client cancellation close it. Invalid ranges close the descriptor before returning HTTP 416. A pathname replacement after opening cannot switch the download to another file.
Managed Project and System downloads adapt readGuestStream(files, path, { offset, length, expectedVersion, expectedTotalBytes }) to a pull-based web byte stream. No content is read before the consumer pulls. The client owns read boundaries, the initial version and size pin, subsequent version checks and the final follow-stat after multi-page reads. A file opened through a symlink is validated against its target on that closing check as well. A failed, changed or incomplete read errors the stream with a sanitized message; it cannot change an HTTP status whose headers were already sent. Cancellation stops subsequent reads and lets at most one bounded in-flight read settle without enqueueing or exposing an unhandled rejection. Empty files need no guest read.
Both paths preserve byte ranges, HTTP 206 and content-range, with the existing 8 MiB maximum range response. The plugin requires core 0.30.7 and shared API 14.
The plugin has no configuration fields. Disabling it removes the editor and its file API; Project registration and task Git history remain available.
Shared file and Git reads
ProjectEditIcon opens the existing icon picker through the host's Button variant="ghost" size="sm", the same control the Git panel uses for Show all. The plugin adds no action-link styles; the host owns hover, focus and disabled presentation.
The Git panel consumes core GET /projects/:id/git; RealGitReader currently returns the newest 15 commits. Editor retains /head, /changed, /changes, /commit/:hash and /commit/:hash/diff. Unused /diff and /commits routes and their parser are removed. src/files.ts owns editorGitArgs and parseGitChangedFiles for both transports; workingDiff disables external diff/text conversion. Authorization, execution and refusal mapping stay transport-owned.
src/fileTypes.ts owns PROJECT_FILE_IGNORE, MAX_EDITABLE_BYTES, MAX_OFFICE_CONVERSIONS, parseByteRange and downloadHeaders. Both raw handlers use its strict single-range parser before streaming: bytes=-3 selects the final three bytes; unsafe suffix/start/end numbers return 416 with content-range bytes */size. Valid ranges keep 206 and the 8 MiB response bound; no Range streams the whole authorized file, including empty files.
useEditorBlobUrl(route, projectId, root, path) in fileData.ts owns ImagePreview/PdfPreview object URLs. It fetches raw or office-preview through the cookie proxy, returns url/failed and revokes on cleanup. Obsolete responses publish no URL. Pending/refusal rendering and preview-size admission remain caller-owned.
Editor runtime declarations include only consumed host members: no Project notes/pr_enabled, EditorComponents ControlSurfaceDocument or utils.baseName. Host publication is unchanged; filename callers use editor/helpers backed by fileTypes. Local retry copy is removed; treeRetry and host common.retry remain.