Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

[English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md) · [简体中文](CHANGELOG.zh-CN.md)

## Unreleased

- Added a rich file viewer to the **Files** card: Markdown files (`.md`, `.markdown`, `.mdown`, `.mkd`) render as formatted GitHub-Flavored Markdown with headings, lists, task lists, tables, blockquotes, horizontal rules, inline code, and fenced code blocks with syntax highlighting, recognized code files are syntax-highlighted by the language detected from their extension while unknown languages stay plain monospaced text, all rendered content is sanitized so raw HTML, scripts, event-handler attributes, and dangerous link schemes never render, `http(s)` links open in the app's built-in browser with other schemes inert, only embedded `data:` images show while remote or relative sources fall back to alt text or a placeholder without a network request, and text and code files above the rich-rendering size threshold fall back to plain text with a notice.
- Added a read-only **Files** canvas card and reader: open it from the empty-canvas context menu or the `Cmd/Ctrl+K` palette, root it at a live terminal session working directory or a folder picked in a native dialog, browse a directory tree that loads on demand, and read text/code as selectable monospace (bounded at 2 MB with a truncation marker and total size) or supported images (bounded at 25 MB) with explicit binary, unsupported, and too-large states. Quick open searches file names inside the active root, each card's root, bounds, open file, and expanded folders persist and restore with an unavailable state when the root is gone, and a new main-process `FileAccessService` enforces `realpath` containment so every listing, read, and search uses relative paths strictly inside registered roots with no write, rename, or delete capability.

## 1.7.1

- Added built-in stable-release updates with explicit download and installation, active-session shutdown confirmation, and signed Sparkle updates on macOS. Install this first updater-enabled release manually; later compatible releases can be installed from the app.
Expand Down
5 changes: 4 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Electron main process
├── PluginAgentTools / PluginSessions / PluginCards → plugin tools in canvastty_agents, session events and owned-card control, card badges and actions
├── PluginSecretsService → OS-backed encrypted plugin credentials with fail-closed availability
├── PluginMediaService → user-granted music folders, ranged audio streams, playlist files
├── FileAccessService → read-only, root-scoped file listing, reading, and search
├── HermesHudService → permission-gated Hermes Desktop HUD lifecycle through a fixed control contract
├── BrowserService → tabs, shared persistent profile, downloads, presence, WebContentsView lifecycle
│ ├── BrowserStore / BrowserPolicyService / BrowserAuditStore
Expand All @@ -51,7 +52,7 @@ Electron main process
- `src/shared/contracts.ts` is the single public contract between processes. Add or change cross-process data here first.
- `src/preload/index.ts` exposes only the typed capabilities the renderer needs. Node integration stays disabled; context isolation and sandbox stay enabled.
- Terminal file drops resolve native `File` objects through preload's `webUtils.getPathForFile`, format paths for the host's default shell, and paste through xterm without submitting. File contents are not read and no new main-process IPC is exposed.
- `src/main/ipc/registerIpc.ts` owns native side effects and validates access to persisted media.
- `src/main/ipc/registerIpc.ts` owns native side effects, validates access to persisted media, and guards the allow-listed `files:*` channels with the trusted-main-renderer check.
- `src/main/services/TerminalManager.ts` is the source of truth for live session state and PTY buffers. It keeps scrollback in a bounded chunk buffer and coalesces PTY data into 16ms IPC batches so clear/redraw sequences reach xterm together. A plain terminal starts `idle`; an agent stays `unavailable` until its provider emits a machine-readable lifecycle signal. Codex, Claude Code, Qwen Code, Kimi Code, OpenCode, Hermes, and Grok Build then transition through `idle`, `working`, and `needs_approval` from provider hooks; exact Claude/Qwen OSC 0/2 markers remain a compatibility fallback. Human-readable terminal text and PTY existence are never treated as activity. Process exit provides only `done` or `failed`. An exited PTY may be restarted under the same session ID while preserving its card, bounds, title, and scrollback. Optional restart persistence (Settings → General, "Agent sessions after restart": Don't save / Reopen windows / Continue conversations) writes session records v2 through `TerminalSessionStore`: the card descriptor, the state at quit or exit, the provider conversation id a lifecycle hook reported (`threadId`: Codex thread or Claude session UUID, OpenCode `ses_` id), the per-card restore flag, and two opaque plugin slots (launch options and an environment reference, at most 4 KB each). It never writes PTY scrollback, prompts, child environment, secrets, or capabilities. Plain terminals reopen as fresh shells in their saved folder.
- `src/renderer/src/features/terminal/webglContextPool.ts` decides which terminal cards draw with xterm's WebGL renderer. Chromium keeps at most 16 WebGL contexts per renderer process, so the pool hands out 10: the focused card first, then by on-screen area (a busy holder keeps its context against a card less than 1.25× larger), then by most recent output. Camera and layout changes settle for 200 ms before contexts move; a card that leaves the screen, zooms above 1× or enters summary mode releases its context (and explicitly loses it) at once. Every other card uses the DOM renderer. A lost context puts the card back on DOM with its buffer intact and keeps it there for 30 s, doubling on repeats; returning to DOM refits the grid, because WebGL cells are snapped down to whole device pixels.
- `src/main/services/LimitsService.ts` reads Codex through the installed CLI's app-server protocol and Claude, Kimi, OpenCode Go, and Grok Build through their provider usage or billing endpoints. Qwen Code is multi-provider and exposes no provider-neutral read-only quota protocol, so its adapter reports `cli-not-found` or `unsupported-protocol` and never invents percentages. Provider credentials are read only inside the trusted main process, sent only to the matching provider over HTTPS, and never logged or exposed over IPC. The service owns timeout, structural normalization, caching, stale fallback, and subprocess cleanup; raw provider responses never cross IPC.
Expand All @@ -64,6 +65,7 @@ Electron main process
- `src/main/services/PluginAgentTools.ts` lists trusted services' `tools` per session role (the orchestration handler answers the helper's `list_tools`; `TerminalManager` gives a non-orchestrator card the `canvastty_agents` bridge only when a plugin tool lists its role) and routes `<pluginId>__<tool>` calls to `canvastty.tools.call`, masked and bounded. `PluginSessions.ts` turns terminal-manager events into `canvastty.sessions.event` notifications (metadata; masked screen text only with `sessions:read-screen`) and runs `sessions.create/send/stop` with per-plugin ownership saved in the card's session record (`ownerPluginId`). `PluginCards.ts` keeps plain-text badges and declared card actions, pushes them to the renderer (`plugins:card-decorations-changed`) and calls `canvastty.cards.invoke`.
- `src/main/services/PluginSecretsService.ts` serializes per-plugin secret writes, encrypts the complete bounded payload through Electron `safeStorage`, rejects plaintext-only backends, and removes each encrypted file on uninstall. `ProviderSecretsService.ts` applies the same architecture to provider API keys for BYOK-capable CLIs: values stay in the main process, and the renderer contract exposes only per-key `configured` flags plus set/clear actions. `ApiProfile` settings entries name model backends (protocol, HTTPS base URL, secret reference) for the same BYOK runtimes; they are not agent providers, and the settings normalizer drops invalid profiles instead of repairing them.
- `src/main/services/PluginMediaService.ts` persists per-plugin grants only after a native folder choice, hides absolute paths, skips symlinks, and serves contained audio with HTTP Range semantics. Playlist reads stay inside granted libraries; writes are bounded and atomic under the library's `Playlists/` directory.
- `src/main/services/FileAccessService.ts` is the single owner of read-only, root-scoped file access. A root is registered either from a terminal session's working directory, which main resolves through `TerminalManager`, or from a folder the user picked in the native dialog. The renderer receives only an opaque `rootId` and a display label, and addresses content with relative paths; the `files:*` channels (`list-roots`, `register-root`, `open-folder`, `list`, `read`, `search`, `close-root`) never carry an absolute path for list/read/search. Every operation canonicalizes root and target with `realpath`, rejects absolute paths, `..` traversal, symlinks that resolve outside the root, and non-regular files, and re-checks containment around the open. Text returns bounded UTF-8 with a `truncated` flag and total size, images return a preview under a MIME allow-list and byte cap, and binary content returns an explicit unsupported reason with no bytes. Directory listings are capped and ordered directories-before-files; search is bounded by depth and result count and skips VCS and dependency directories. Roots are released when their card closes and re-validated on restore, and there is no write, rename, or delete path.
- `src/main/services/HermesHudService.ts` is the only plugin-facing native application controller. It resolves the installed Hermes CLI through the immutable provider registry, sends only the fixed `--hud`/`--quit` control commands, and derives visible state from Hermes Desktop's validated live runtime record. It never accepts executable paths, arguments, PIDs, or arbitrary commands from plugin code.
- `src/main/services/BrowserService.ts` is the only owner of the built-in browser's `WebContentsView` tabs and shared persistent partition. Remote pages have no preload or Node access, keep context isolation and sandbox enabled, and cannot request hardware, location, notification, clipboard-read, certificate-bypass, or external-protocol capabilities. HTTP(S) popups are adopted as internal tabs; other schemes are rejected.
- `src/main/services/browser/` contains the browser kernel. `BrowserStore` atomically persists only tab order, active tab, and safe restore URLs. `BrowserPolicyService` centralizes URL, permission, download, and upload rules; validated uploads are copied through an already-open no-follow file descriptor into private staging before Chromium sees them. `BrowserAutomationService` attaches Electron's internal debugger to the existing live tab without a remote-debugging port. `BrowserCommandDispatcher` adds revisions, revision-bound refs, mutation request deduplication, per-tab FIFO mutation lanes, bounded concurrency, typed errors, and redacted fail-closed audit for agent mutations.
Expand Down Expand Up @@ -102,6 +104,7 @@ App
│ ├── CanvasCommandPalette searchable sessions and the same global creation/launch actions
│ ├── PluginCanvasCard sandboxed plugin app with canvas bounds and semantic summary
│ ├── BrowserCard trusted browser chrome and canvas geometry for the native WebContentsView
│ ├── FileBrowserCard read-only tree and viewer for files inside a registered root
│ └── CanvasMinimap viewport/entity overview, camera recentering, and canvas-direction drag panning
├── AgentLaunchDialog fixed provider + folder + profile + launch
└── SettingsPanel two-pane icon-sidebar modal for General, Appearance, Agents, Controls, Browser, Plugins, and About
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CanvasTTY is a spatial Electron desktop for real local terminals and AI-agent CL
|:--|:--|
| [Getting started](getting-started.md) | Requirements, local launch, first session, and verification commands |
| [Built-in browser and audit log](browser.md) | Canvas controls, settings, agent access, website/file boundaries, activity, and persistent redacted audit files |
| [File browser and reader](files.md) | Opening a Files card, session and folder roots, supported content, read limits, quick open, persistence, and read-only security boundaries |
| [Web companion](mobile-companion.md) | Tailscale Serve HTTPS setup, browser pairing, session grants, revocation, and security limits |
| [Installing, releases, and local data](installing-and-security.md) | Installer formats, unsigned-preview caveats, credential boundaries, and release checks |
| [Widget authoring](widget-authoring.md) | Source-level extension paths, visual grammar, process boundaries, and an AI-agent brief |
Expand Down
2 changes: 2 additions & 0 deletions docs/UI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ This contract preserves the approved MVP concept and prevents feature ownership
- Controls includes an off-by-default **Copy on selection** setting, persisted and applied immediately to every agent and terminal, including fullscreen cards. Completing a non-empty mouse selection copies its text to the system clipboard without clearing the highlight; search results and cleared selections do not copy. With the setting enabled, Shift forces text selection in mouse-reporting TUIs on all desktop platforms; on macOS the terminal mouse adapter translates it to xterm's Option selection without taking the canvas's Option navigation binding.
- Canvas plugin apps use the same movable card grammar, `54px` header, resize/snap behavior, and semantic summary below `0.5×`. A `window` contribution opens a separate CanvasTTY-owned sandboxed window; arbitrary native window embedding is not part of the contract.
- The built-in Browser is one movable, resizable core canvas card, not a plugin contribution. It uses the same `54px` outer window header as other canvas cards, with its identity and hide-card action isolated from the internal tab strip below. Its trusted DOM chrome owns compact tabs/favicons, address/search, back/forward/reload, downloads, per-tab provider badges, site dialogs, and explicit Close tab/Close all. Hiding the card keeps its tabs and shared authenticated Chromium profile alive. Below `0.5×`, during Edit HOME, behind trusted dialogs/popovers, and while overlapped by a higher renderer-owned canvas window, the native page is hidden and replaced with a stable semantic surface. During card or camera motion it remains live and follows frame-coalesced viewport geometry.
- The Files card is a movable, resizable trusted canvas card using the same `54px` header, edge/corner resize, snapping, focus, stacking, minimap marker, and close grammar as other cards. Its header shows the root label and the active file; its body pairs a tree that expands directories on demand with a read-only viewer, and quick open searches file names inside the active root. Text and code render as selectable monospace text with a truncation banner, supported images render as a preview, and binary, oversized, unavailable, or out-of-root content shows an explicit non-content state with the reason. Below `0.5×` it collapses to the shared semantic summary. It never exposes a write, rename, or delete action.
- The Files viewer renders Markdown (`.md`, `.markdown`, `.mdown`, `.mkd`) as formatted GitHub-Flavored Markdown with fenced code blocks syntax-highlighted, and syntax-highlights recognized code files by the language detected from their extension while unknown languages stay plain monospaced text. All rendered output is sanitized: raw HTML and scripts never render and event-handler attributes and dangerous link schemes such as `javascript:` are neutralized. Activating an `http(s)` link opens it in the built-in browser while other schemes are inert, only embedded `data:` images render while remote or relative sources fall back to alt text or a placeholder without a network request, and text or code above the rich-rendering size threshold falls back to plain text with a notice.
- An authenticated connection or heartbeat alone never shows agent presence. A branded badge appears only after that agent actually issues a browser command, and its cursor appears only after a real pointer position exists. Claude uses `#D97757`, Codex `#10A37F`, Qwen `#6D44E8`, Kimi `#7C5CFC`, OpenCode `#5A5858`, Hermes `#D6A700`, and an unknown provider `#7A8291`. Browser Settings separately controls indicator visibility without revoking browser access; the agent-access kill switch revokes the connection itself.
- With window snapping enabled, drag and resize use a hidden `10px` grid and a `10px` magnetic threshold for neighboring edges, centers, and a consistent `20px` gap.
- One trusted context dispatcher owns right-clicks on the canvas. Truly empty canvas offers region creation, a sticky note, the Settings-configured launcher submenu, Browser, and Settings. Holding RMB on empty canvas also opens the configurable radial quick launcher: moving toward an action and releasing activates it, while a stationary right-click continues to open the standard empty-canvas menu. Region and note right-clicks remain context-specific. A region offers rename, color, a note at the pressed point, and region-only deletion. A note frame offers edit, bring-to-front, and deletion. Terminal, Browser, plugin, textarea/input, and contenteditable surfaces keep their native context menus; middle-button panning is unchanged. `Cmd/Ctrl+K` opens a searchable palette over live sessions and the same global creation/launch actions. Regions persist independently of terminal restore, render behind HOME and windows, resize from every edge/corner, and move every terminal, plugin, Browser, or note whose complete bounds were inside the region at drag start. Attached contents follow the region continuously during the gesture and persist once at pointer release; deleting a region never deletes its contents. Sticky notes persist bounded text and geometry, snap like other windows, and resize from every edge/corner. General Settings exposes independent **Save colored regions after exit** and **Save notes after exit** controls. Both default on to preserve existing profiles. Turning either off removes only that collection from disk persistence without deleting its live canvas objects; turning it back on saves the current collection, while leaving it off starts the next launch with that collection empty.
Expand Down
Loading
Loading