Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/UI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ This contract preserves the approved MVP concept and prevents feature ownership
- 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.
- Context menus, region editors, the radial launcher, and the command palette use official Lucide/provider assets and shared application color tokens. Their dimensions follow root `--ui-scale`. Appearance owns the `0.85–1.25` scale in `0.05` steps and the sage/lilac/night system palette; the scale changes UI chrome rather than canvas zoom or terminal font size. Agents owns the independent context-menu and radial-launcher subsets and their safe reset defaults.
- Context menus, region editors, the radial launcher, and the command palette use official Lucide/provider assets and shared application color tokens. Their dimensions follow root `--ui-scale`, as do the terminal card options menu and the canvas shortcut-hints panel. Appearance owns the `0.85–1.25` scale in `0.05` steps and the sage/lilac/night system palette; the scale changes UI chrome rather than canvas zoom or terminal font size. Agents owns the independent context-menu and radial-launcher subsets and their safe reset defaults.
- Settings uses the approved two-pane modal grammar: a persistent icon-labelled sidebar for all seven sections, a separate current-section header, and a scrollable content pane. Settings must not fall back to the legacy light slide-over with one horizontal segmented tab strip. Simple settings use compact copy/control rows; all Settings chrome reads the active system tokens, official Lucide assets, and root `--ui-scale`.
- General Settings places the macOS/Windows/Linux/Custom keyboard preset after language, followed by an expandable Keyboard shortcuts section for canvas, terminal, Codex editor and canvas navigation bindings. Recording a shortcut selects Custom; choosing a named preset replaces bindings. Optional commands can be disabled, while submit, newline and whole-draft selection remain bound. Canvas and terminal bindings update immediately; native Codex editor bindings apply to new or restarted cards. Focused terminal and text inputs retain ownership of canvas keyboard shortcuts on every platform.
- Empty-canvas drag and middle-button drag pan the canvas. On fresh profiles ordinary scroll over empty canvas pans on both axes; pinch and `Cmd/Ctrl + scroll` zoom around the pointer. **Use scroll wheel to zoom** restores historical ordinary-wheel zoom and is enabled by migration for existing profiles. Only a focused input widget interrupts canvas wheel/pinch, except the HOME session list keeps ordinary local scrolling without a focus precondition; unfocused or non-focusable widgets otherwise leave those events to the canvas. **Wheel/pinch capture over widgets** has Off, On, and Key modes: Off preserves local ownership, On always transfers only wheel/pinch to the canvas, and Key transfers only wheel/pinch while its binding is held. Key and full-navigation bindings accept modifier chords or Mouse3/Mouse4/Mouse5. A focused live Browser page therefore scrolls normally in Off or released-Key mode; unfocused Browser content, On, an active Key binding, the full override, pinch, and `Cmd/Ctrl + scroll` are canvas-owned. The owner selected for a Browser wheel sequence is held until 250 ms of wheel inactivity. Browser summaries and placeholders are always canvas-owned. Fresh profiles use Key with `Command` on macOS or `Ctrl` elsewhere. A separate configurable **Canvas navigation override** (default `Option` on macOS and `Alt` elsewhere) transfers wheel/pinch and drag while held. While full override is held all canvas surfaces show `grab`; an owned drag shows `grabbing` until pointer up/cancel even after the binding is released.
Expand Down
6 changes: 6 additions & 0 deletions docs/canvastty-plugin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"apiVersion": { "enum": [1, 2] },
"id": { "type": "string", "minLength": 3, "maxLength": 80, "pattern": "^(?!.*\\.\\.)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$" },
"name": { "type": "string", "minLength": 1, "maxLength": 80 },
"name.ru": { "description": "The name when the app is in Russian (like description.ru); name otherwise.", "type": "string", "minLength": 1, "maxLength": 80 },
"name.en": { "description": "The name when the app is in English (like description.en); name otherwise.", "type": "string", "minLength": 1, "maxLength": 80 },
"version": { "type": "string", "maxLength": 40, "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
"description": { "type": "string", "minLength": 1, "maxLength": 2000 },
"description.ru": { "type": "string", "minLength": 1, "maxLength": 2000 },
Expand Down Expand Up @@ -261,6 +263,8 @@
"properties": {
"key": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,39}$" },
"label": { "type": "string", "minLength": 1, "maxLength": 80 },
"label.ru": { "description": "The label when the app is in Russian (like description.ru); label otherwise.", "type": "string", "minLength": 1, "maxLength": 80 },
"label.en": { "description": "The label when the app is in English (like description.en); label otherwise.", "type": "string", "minLength": 1, "maxLength": 80 },
"kind": { "enum": ["boolean", "select", "text"] },
"options": {
"type": "array", "minItems": 1, "maxItems": 16,
Expand All @@ -285,6 +289,8 @@
"id": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$" },
"kind": { "enum": ["home-widget", "canvas-app", "window"] },
"title": { "type": "string", "minLength": 1, "maxLength": 80 },
"title.ru": { "description": "The title when the app is in Russian (like description.ru); title otherwise.", "type": "string", "minLength": 1, "maxLength": 80 },
"title.en": { "description": "The title when the app is in English (like description.en); title otherwise.", "type": "string", "minLength": 1, "maxLength": 80 },
"description": { "type": "string", "minLength": 1, "maxLength": 240 },
"entry": { "type": "string", "minLength": 1, "maxLength": 180, "pattern": "^(?!/)(?!.*\\\\)(?!\\.\\.?/)(?!.*(?:/\\.\\.?/|/\\.\\.?$|//)).+\\.html$" },
"icon": { "type": "string", "minLength": 1, "maxLength": 180, "pattern": "^(?!/)(?!.*\\\\)(?!\\.\\.?/)(?!.*(?:/\\.\\.?/|/\\.\\.?$|//)).+$" },
Expand Down
12 changes: 10 additions & 2 deletions docs/plugin-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export interface CanvasTTYPluginContext {
appearance: {
locale: "ru" | "en";
palette: "sage" | "lilac" | "night";
/** The app's UI scale (Settings → Appearance; 1 is 100%, from 0.85 to 1.25), the value CanvasTTY scales its own
* text and controls by. Sent again whenever the person changes it. */
uiScale: number;
};
}

Expand Down Expand Up @@ -392,15 +395,20 @@ export interface CanvasTTYServiceLaunch {
delegable?: boolean;
}

export type CanvasTTYLaunchField =
export type CanvasTTYLaunchField = {
/** The label when the app is in Russian / English (up to 80 characters, like `description.ru`); `label` otherwise. */
"label.ru"?: string;
"label.en"?: string;
} & (
| { key: string; label: string; kind: "boolean"; default?: boolean }
| {
key: string; label: string; kind: "select"; options: Array<{ value: string; label: string }>; default?: string;
/** The launcher also asks `canvastty.launch.options` (3 s) for up to 64 more choices; the saved value is then any
* text up to 200 characters, which `canvastty.launch.prepare` must check. */
optionsFrom?: "service";
}
| { key: string; label: string; kind: "text"; maxLength?: number; default?: string };
| { key: string; label: string; kind: "text"; maxLength?: number; default?: string }
);

/** Params of the host request `canvastty.launch.prepare`: launches that chose this plugin, and with `policy` every agent launch. */
export interface CanvasTTYLaunchContext {
Expand Down
6 changes: 4 additions & 2 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ Plugin and contribution IDs are stable persistence keys. Do not rename them afte

`platforms` is optional; when present it must include `"canvastty"` or direct install/update is rejected. `minHostVersion` is informational: the showcase marks plugins that target a newer host, but it does not block installation. This lets separately packaged release builds keep using compatible source packages without treating older minimum versions as mismatches.

Texts the person reads may come in the app's languages, like `description.ru`/`description.en`: the plugin `name` may add `"name.ru"` and `"name.en"`, a contribution's `title` (a canvas app, the settings page, a HOME widget, a window) may add `"title.ru"` and `"title.en"`, and a launch field's `label` may add `"label.ru"` and `"label.en"`. Each has the length limit of the text it translates (80 characters). CanvasTTY shows the one for its current language, and the plain text otherwise. Other suffixes are rejected.

### Optional modules

A modular manifest declares integrity-checked coreFiles plus up to 16 optional modules. Every file entry contains path, exact bytes, and a SHA-256 digest. CanvasTTY downloads only the manifest for inspection, shows checkboxes, per-module size and permissions, then downloads only the core and selected module files. Changing the selection later replaces the installed package atomically and removes deselected files. A contribution may set module to disappear when that module is not installed.
Expand Down Expand Up @@ -192,7 +194,7 @@ One service per plugin may add a `launch` block. Its fields appear in the agent
}]
```

Up to 8 fields; `kind` is `boolean`, `select` (1–16 options) or `text` (at most 200 characters, or `maxLength`). `appliesTo` lists agent providers; omitted means every agent. The chosen values are checked against the fields, saved in the card's session record (at most 4 KB per plugin), and reused on restart and restore. They are not secret: put keys in the plugin's `secrets`, never in a field.
Up to 8 fields; `kind` is `boolean`, `select` (1–16 options) or `text` (at most 200 characters, or `maxLength`). `appliesTo` lists agent providers; omitted means every agent. The chosen values are checked against the fields, saved in the card's session record (at most 4 KB per plugin), and reused on restart and restore. They are not secret: put keys in the plugin's `secrets`, never in a field. A field's `label` may add `"label.ru"` and `"label.en"` (see the manifest above).

A `select` with `"optionsFrom": "service"` also lists choices the service offers, such as its own accounts. When the launcher opens, CanvasTTY asks the service `canvastty.launch.options` `{ provider, fields: [keys] }` and waits at most 3 s; the answer `{ "<key>": [{ value, label }] }` adds up to 64 choices per field after the declared ones (which stay required and are all the launcher shows when the service does not answer). Because such a list can change after a card was saved, its value is accepted as any text up to 200 characters without control characters, and `canvastty.launch.prepare` must check it and refuse a value it no longer knows.

Expand Down Expand Up @@ -527,7 +529,7 @@ const current = host.visibility(); // "visible" | "hidden"

Plain `document.addEventListener("visibilitychange", …)` works too, without the SDK. The host message behind it is `{ source: "canvastty-host", type: "visibility", state: "visible" | "hidden" }`; plugins do not need to handle it themselves.

Context updates include the active CanvasTTY locale and palette. Plugins own their internal localization and styling; they should remain legible at the contribution's intended size and should not invent loading progress, sessions, status, limits, or telemetry.
Context updates include the active CanvasTTY locale and palette. Plugins own their internal localization and styling; they should remain legible at the contribution's intended size and should not invent loading progress, sessions, status, limits, or telemetry. `appearance.uiScale` is the app's UI scale (Settings → Appearance; 1 is 100%, 0.85 to 1.25), the value CanvasTTY scales its own text and controls by; a page or canvas app that should match the app scales its text by it, for example `` document.documentElement.style.fontSize = `${13 * appearance.uiScale}px` ``. A new context arrives whenever the person changes the locale, the palette or the scale; a separate window reads it when it loads.

## Install and manage

Expand Down
Loading
Loading