Skip to content

Build a workspace app

How an agent declares, installs, configures and operates a first-party Tedix OS workspace app, with the Card review app as a worked example.

Agent setup

A workspace app (called a gadget in code and in API names) is a versioned record in one Workspace. Its manifest says what the app is; its revisions are append-only. There are three kinds of app, chosen by the manifest entry:

entry What renders
tedix:app/<id> A built-in OS app, rendered first-party with no iframe
ui://widgets/mcp-app/<slug>/… An MCP App widget, rendered in the sandboxed widget host
a skill slug (or skillSlug) A governed skill run from the app’s Runs view

A built-in app keeps every tenant choice in the manifest config, never in code. The OS validates config against the app’s schema when a revision is written, so a wrong setting fails the write with the exact path that is wrong.

Office (tedix:app/office) and Agents (tedix:app/agents) are built-in apps. Both require config: {"version": 1}. Office shows the viewer’s live activity, reply outcomes, leaderboard and notebook. Agents accepts optional projectId, states (needs_you, error, done, working, idle, ended) and harnesses (codex, claude-code, tedi, skill). Its screen filters narrow these settings; counts and delegation totals use the same filtered sessions. The organization and viewer access checks remain authoritative; app filters never grant access. Install and opt these apps into the Feed’s Apps list to make them visible.

Install and configure an app

From chat or MCP, use the OS app tools in the organization’s namespace (discover the exact callable first):

  1. create_os_gadget with workspaceId, name and an optional description. This creates the record only. The name is the app’s title in the workspace, so make it say what the app is (“Card review”).
  2. revise_os_gadget with workspaceId, gadgetId and the manifest (entry, config and a one-line description). Pass expectedRevision (0 for a new app). Each change of settings is a new revision; nothing is edited in place.
  3. Read it back with get_os_gadget and give the user the app link: https://<org>.tedix.dev/workspace/<workspaceId>?workpiece=gadget:<gadgetId>. Add &pane=workpiece to open it full screen on a phone.

In the OS, the same settings are editable under the app’s Code view (manifest.json).

For one read-only consent button covering several personal accounts, use entry: "tedix:app/personal-read-consent". Its config contains version: 1, the scheduled skill’s skillId, its Workspace resourceId, a workerName, and providers: [{id, name}] (for example, google-gmail / Gmail and google-calendar / Calendar). The person must click the button while signed in as the Workspace owner. Each provider is checked against the skill’s declared, reviewed read tools; an unconnected account links to Settings → Connections. Partial consent remains visible and can be retried. Revoke each account’s access in Resources. Installing the app grants no access.

Set navigation: {"board": true} in a manifest to show that active app in Work → Feed → Apps. Its stored name becomes the link label; it opens the same workspace resource. Only apps in workspaces the viewer can access appear. Omit the setting, set it false, or archive the app to remove the app link.

tedix:app/outbound-review lists outbound Work drafts with status and search filters and the existing review, feedback and human approval/send controls. Its config is {"version": 1, "limit": 50} with optional projectId and initialStatus (for example waiting_on_you). Omitting projectId shows the viewer’s organization-wide list. Configuration never grants permission to send; the existing API checks and version fences still apply.

How apps appear in a workspace

Apps are what people open a workspace for, so the OS puts them first:

  • Resources lists apps at the top under Apps, one card per app, side by side when there are several. Runs on a schedule follows, then sheets and documents under Files. Nothing in a workspace is labelled “Automations”.
  • Each card shows a small preview of the app, its name, its purpose and a status line, with an Open app button. Card review previews the next open card; other apps show an app icon.
  • A workspace link with no workpiece or pane opens the first app in the Editor. An app whose entry is one of the workspace’s outputs opens that output’s editor instead. A link that names a workpiece or pane always wins.

The card’s purpose line comes from, in order: the manifest description (at most 160 characters, versioned with the app), the app record’s description, then a line derived from the settings (for Card review, “Decide on today’s Inbox and Social cards” from the source labels). Write it as what a person does in the app:

{
	"entry": "tedix:app/card-review",
	"description": "Decide on today's Inbox and Social cards",
	"capabilities": [],
	"config": { "version": 1, "sources": [] }
}

The status line is computed by the OS from live data; the manifest does not set it. Card review counts the open cards across its sources (“29 cards waiting”), without subtracting a viewer’s snoozes. Other apps show their kind: “Built-in app”, “Interactive app” or “Runs a skill”.

Runs on a schedule lists the scheduled skills that fill the workspace, such as the factory that writes an app’s cards. Each row shows the skill’s name and purpose, when it runs in the viewer’s own clock (“Every day at 8:17, 13:17 and 18:17”), the last run (“Last run 13:17: made 4 cards” or “Last run failed: ”), the next run and a See runs link. A skill is listed when the workspace has a resource with providerId tedix, resourceType skill and the skill ID as providerResourceId; create_factory with a Workspace adds it. Its name is the row title. Optional metadata: purpose (one line) and runsAt ({"timeZone": "Europe/Berlin", "times": ["08:17"]}) for a skill whose UTC cron fires at both daylight-saving hours and skips the one that is not its local time. Runs whose result status is off_slot are not shown; a result’s cards count or one-line summary becomes the last-run text. The section is hidden when a workspace has no scheduled skill, and these links are not listed under Connected sources.

When a scheduled skill reads its owner’s own account, the row also shows, to the workspace’s owner only, Let this read my Gmail (read-only). One click picks the owner’s connected Gmail account (or links to Settings → Connections when there is none), attaches it to the workspace and lets the skill’s worker search that mailbox, read-only, until the owner clicks Stop on “Reads your Gmail (read-only) · Stop”. The skill says what it needs in its own manifest: a provider tool such as google_gmail: [search_threads] whose installed tool uses a personal connection and is a reviewed tool of the account. Editing the skill keeps the grant.

Attaching your own account to a workspace is the permission. Its row under Attached resources has one choice, What this workspace’s skills may do with it: No access, Read, or Read + organise (archive, label, unsubscribe; never sends or deletes). Only the account’s owner can change it (osWorkspaces.schedules.setPersonalAccess). The choice covers every skill the workspace runs that declares tools for that account, each with exactly its declared tools of that level; the schedule row then says “Reads your Gmail (read-only)” or “Reads and organises your Gmail (never sends or deletes)”. For Gmail, read is search_threads, get_thread, get_message and list_labels; organise adds label_thread, unlabel_thread (archive and mark read), create_label and create_draft. No level reaches a send, trash, spam or delete tool, and every delegated call that would add the Trash or Spam label is refused. A private workspace can be given to a member: its owner, or an organization admin for an org workspace, updates it with visibility: "private" and that member’s ownerUserId. Afterwards only that member sees it, and a scheduled skill linked to it can read and write its outputs only while a run is going.

Card review

tedix:app/card-review shows a factory’s card sheets one card at a time: who and where, what happened and what Tedix suggests, any draft, and the buttons Useful, Not useful, Do it, Snooze and Skip. Rule ids stay behind Details. Swipe right or left on a phone; on a keyboard use ←/→, D, S, K, the number keys for reason chips and Z to undo.

Write each card in plain words, at most two short lines: who, what happened and what Tedix suggests (“Mia asked if Tuesday works. Tedix suggests a short yes.”). Rule ids may follow in the why cell, for example (R4); the card hides them.

Verdicts go into the card sheet’s own cells: the useful column gets yes or no, the reason column gets the chips and note as Chip, Chip: note, and the done column gets yes from Do it (no from Not useful). A factory that already reads those cells keeps working unchanged. Snooze is per viewer and writes nothing.

Do it is named after the card’s action cell, a short concrete label of at most 40 characters such as “Draft a reply to Mia” or “Add to calendar”. A card with an empty action cell has no Do it button. A sheet without the action column, or a longer action sentence, uses doItLabel (default “Do it”, or “Copy draft and open” for copy_and_open).

Do it never sends mail, posts, trashes or deletes. approve marks the done column; the factory or the person carries the action out. copy_and_open copies the draft, opens the source link and marks the done column.

run_skill carries the action out at once. The button shows “Doing it: …” while a run of run.skillId works on that card (osWorkspaces.schedules.runNow), then the outcome: Done ✓, Draft ready, Filed as Work with a link to the item, or Failed: and the reason. The run writes the outcome into the result column (default Result, added to a sheet that lacks it) and, when it worked, yes into the useful and done columns, so the card leaves the queue. A failed card stays with “Last try: Failed: …”. The skill must be linked to the source workspace under Runs on a schedule.

run.actions maps a card to a kind; the first entry whose match text is in the card’s type cell, then its action cell, wins, and anything else is work:

Kind What the run does
archive Removes the Inbox label from the card’s threads
mark_read Removes the Unread label
label Adds the label named in the action (“Label as Receipts”), creating it if needed
unsubscribe Uses the sender’s one-click unsubscribe when the mail offers it, then archives
reply_draft Saves the card’s draft as a reply draft in the same thread; it is never sent
bundle Runs each tidy-up a housekeeping card lists (“3 tidy-ups: Unsubscribe (Weekly News); Label as Receipts (Shop)”) on its own thread, mapped by these same rules; the rest become one Work item
work Files a Tedix Work item in run.workProjectId with the card’s context and links

Every kind except work changes the owner’s mailbox, so it needs Read + organise on their account. Without it nothing runs and the card shows Allow organising first with the owner’s own button, Let this organise my Gmail (archive, label, unsubscribe; never sends or deletes). A Work item names the source’s tedi as steward, so that tedi or a local Claude or Codex agent picks it up.

Rules-sheet rows with status proposed appear above the cards under Suggested change, with Yes, change it and No, keep as is, which set the status to accepted or rejected. Write the rule as one plain sentence starting “Stop showing…” or “Also show…”, then “Why: …”, under 35 words in all. The rule id stays behind Details.

Example config

{
	"entry": "tedix:app/card-review",
	"description": "Decide on today's Inbox and Social cards",
	"capabilities": [],
	"config": {
		"version": 1,
		"sources": [
			{
				"label": "Inbox",
				"workspaceId": "11111111-1111-4111-8111-111111111111",
				"cardSheetPrefix": "Inbox cards ",
				"maxRuns": 3,
				"columns": {
					"number": "#",
					"type": "Card type",
					"source": "Thread",
					"action": "Proposed action",
					"why": "Why",
					"draft": "Draft",
					"useful": "Useful?",
					"reason": "Reason",
					"done": "Approve action?"
				},
				"doIt": "run_skill",
				"run": {
					"skillId": "inbox-actions",
					"workProjectId": "33333333-3333-4333-8333-333333333333",
					"actions": [
						{ "kind": "bundle", "match": ["housekeeping", "tidy up"] },
						{ "kind": "unsubscribe", "match": ["unsubscribe"] },
						{ "kind": "label", "match": ["label"] },
						{
							"kind": "reply_draft",
							"match": ["reply draft", "draft a reply"]
						},
						{ "kind": "archive", "match": ["archive"] },
						{ "kind": "mark_read", "match": ["mark read", "mark as read"] }
					]
				},
				"rules": {
					"sheetTitle": "Rules",
					"idColumn": "Rule id",
					"textColumn": "Rule",
					"statusColumn": "Status"
				},
				"feedbackSheetTitle": "Feedback log",
				"reasonChips": {
					"useful": ["Good catch", "Saves time"],
					"notUseful": ["Wrong person", "Not timely", "Keep this mail"]
				}
			},
			{
				"label": "Social",
				"workspaceId": "22222222-2222-4222-8222-222222222222",
				"cardSheetPrefix": "Social cards ",
				"columns": {
					"number": "#",
					"type": "Type",
					"source": "Source",
					"who": "Who",
					"why": "Why",
					"action": "Action",
					"draft": "Draft",
					"useful": "Useful?",
					"reason": "Reason",
					"done": "Did it?"
				},
				"doIt": "copy_and_open",
				"channels": [
					{ "label": "LinkedIn", "match": ["linkedin.com", "linkedin"] },
					{ "label": "X", "match": ["x.com", "twitter.com"] }
				]
			}
		]
	}
}

Column names match sheet headers case-insensitively, exactly or as the start of a header, so Thread finds Thread (sender · subject · Gmail link). One app may read several Workspaces; the viewer must be able to open each one.

Review cards from chat or MCP

The app holds no data of its own, so an agent reviews with the output tools:

  • “Show my next card”: list_os_outputs for the source Workspace, take the newest sheet whose title starts with cardSheetPrefix, then get_os_output and pick the first row whose useful, reason and done cells are all empty.
  • “Mark it useful: great angle”: set_os_sheet_range on that sheet with the row index as startRow, the useful column’s index as startColumn, and cells: [["yes", "Great angle"]] when the reason column follows it. Pass the revision you read as expectedRevision; on a conflict, read again.
  • “Carry out this card’s action and show the result”: read the app’s current source config and card row first. For run_skill, discover the callable for osWorkspaces.schedules.runNow and pass its Workspace, run.skillId, card params described in the factory guide, the mapped kind, and a fresh UUID idempotencyKey. Set needs: "organise" for mailbox actions. Reuse that key only to retry the same request. A needs_access result means nothing ran: point the owner to Attached resources → Read + organise. Chat cannot grant another person’s account. Follow the run and read the result cell and provider record before reporting success. For approve or copy_and_open, explain that the decision or draft was recorded rather than claiming execution.
  • “Do the housekeeping card”: use the same run with kind: "bundle" and the source’s run.actions. Check each listed tidy-up’s result and the linked Work for anything the skill could not map. Do not turn the bundle into a request for the person to clean up agent Work.

Every app build must update its agent guide with these chat operations, required access and result checks. Test the chat path alongside the app’s buttons; a WebMCP tool is available only while its app is open.

Decision deck

While the app is open, get_decision_deck_card reads the displayed card, options, consequences, reasoning, cited lessons and readiness. It is read-only; a browser agent cannot answer or approve human decisions.

tedix:app/decision-deck uses the same workspace renderer. Its manifest settings are version: 1, optional projectId, limit (default 50), unchangedThreshold (5), dailyAttentionBudget (10) and auditSampleRate (0.1). defaultLowStakesToDelegate (true) makes Decide for me the primary response for reversible taste choices with no protected effects. Optional examples hold up to twelve fictional decision envelopes for the separate Try examples preview. Practice choices stay in the preview and never train the person’s profile or approve real work.

The live deck reads the viewer’s existing Interaction inbox and approvals. Existing outbound cards retain their reviewed-version and consent checks. Architecture and strategy requests can supply metadata.decisionDeck: proposal, why now, recommendation, reversibility, protected effects, blast radius, confidence, cost of waiting, reasoning, cited lessons and 2–4 options with consequences and costs. The versioned schema is packages/api-contract/src/schemas/os-decision-deck.ts.

An option can carry a typed effect: accept_work, complete_work, record_adr, record_review, file_request, publish_docs or publish_support. These reuse the original product’s permissions. A card can show the exact draft in preview.body; a Support draft pins the ticket version, a document review pins its revision, and a filing correction pins its previous destination. The server saves an action claim before starting. An unconfirmed result leaves the request open and blocks another execution; check the saved receipt and target before preparing a new decision.

New Support drafts create Outbound cards for the configured human admin. Automatic filing creates reviews only for uncertain matches. The owner’s decision-filing-reviews setting in the organization’s user-config namespace controls enabled (true), confidenceBelow (0.8), and limit (10). Choosing No, refile it returns the request to Unfiled and records the correction for the existing learning process. Decisions retain the proposal, project, timing, waiting cost and outcome with the person’s feedback; explicit feedback can teach immediately, while an isolated approval remains an observation rather than a general rule.

Open Deck to finish a short batch of decisions, then get back to your day. Set navigation.work to "deck" on the installed Deck app and to "feed" on the Feed app. Work opens the Feed on phone and desktop; Decide opens the Deck. Both use the generic workspace app renderer and the viewer’s existing access.

The batch has a fixed list: new arrivals wait for the next batch instead of extending it. batchTimesUtc defaults to ["09:00", "16:00"]; the completion message displays the next time in the viewer’s timezone. dailyAttentionBudget caps each person’s remaining batch. Set gamification: false to hide the streak and daily goal; dailyGoal defaults to 7. The current browser tab remembers deferred cards for that batch. These attention controls do not replace server permissions.

Swipe or use the visible buttons. Desktop shortcuts are Left for Not this, Right for Approve, E for Change, L for Later, D for Decide for me and ? for Why this, when that action is available. Typing in a field never triggers these shortcuts. Right always means Approve, even when Decide for me is the primary button. Consequential choices retain their confirmation.

Readiness counts distinct recorded human decisions for this person and class. In the person’s organization-scoped reply policy, classPolicies.<class>.autoApprove enables earned delivery for reversible classes without protected effects or strategy decisions. The class’s unchangedThreshold defaults to 5 and auditSampleRate to 0.1. A rejection or correction resets the count. Audit samples return to review. Existing autoSend permission, session limits and independent delivery checks still apply; the immutable reply draft remains the receipt. This does not enable automatic approval of money, messages, production or consent.

The Deck’s classPolicies settings control its readiness display; changing that display alone cannot enable delivery. The weekly learning digest in Office and decision history links back to the choices. Agents and Office show time to decide, Decide-for-me rate, cards today, return visits today and correction rate from recent personal activity. Loaded history is bounded and is labeled as such. Cost of waiting comes from the decision’s source; expiry uses the request’s deadline, or its decision envelope’s expiresAt. The live “nobody is carrying this” signal requires a current Work read: no accountable owner and no holding Attempt. A failed, pending or older read does not imply missing ownership. This is separate from whether a person is online.

From chat, discover the Interaction inbox/detail tools and read the exact request and version. Explain the offered responses and consequences; do not answer or approve a human’s card. A saved response proves the choice was recorded. Follow the originating Work and provider receipt to check execution. History is read-only in the deck. Why this? shows reasoning and cited lessons; the account profile remains the style-profile surface.

Expose WebMCP tools

A built-in app registers its own WebMCP tools while it is open, so a browser agent in the person’s Tedix OS tab can work the app with them. The person watches each action: the card changes, the chips light up and the count moves, because every tool calls the same state and callbacks as the app’s buttons. The tools disappear when the person closes the app or opens another workpiece.

Card review registers these tools:

Tool What it does
card_review_next_card Reads the open card (source, type, who, why with its rules, action, draft, link, sheet and row), the Do it label, cards left, filter and reason chips
card_review_set_verdict Marks the open card useful or not_useful, with optional reason chips and text
card_review_do_it Presses Do it; for run_skill it waits and returns the outcome as result. It never sends or deletes, and fails when the card has no action
card_review_snooze Snoozes the open card until tomorrow for this viewer
card_review_skip Moves the open card to the end of the queue
card_review_undo Undoes the last action, like the Undo button
card_review_filter Shows all cards or one channel (for example linkedin or x)
card_review_pending_rules Lists the suggested rule changes
card_review_decide_rule Accepts or rejects one suggested rule change

The verdict, Do it and skip/snooze tools take the cardKey that card_review_next_card returned and fail if a different card is open, so an agent never writes onto a card the person has just moved past.

To give a new app tools:

  1. Put the tool set in apps/os/src/components/<app>-webmcp-tools.ts as a pure builder that takes a current() getter for the app’s live state and callbacks. Name every tool <app_id>_<verb>… in snake_case, give it a plain description and an input schema, and set readOnlyHint on reads and untrustedContentHint on reads that return tenant content.
  2. In the app component, pass that state and the same callbacks the buttons use to useAppWebMcpTools("<app-id>", controller, build, deps) (apps/os/src/lib/webmcp/use-app-webmcp-tools.ts). deps lists only what changes the tool shape, such as a filter enum.
  3. Return each callback’s outcome so a tool can report a failed write as an error; execute never throws.
  4. Test it by rendering the app with a mocked osApi, executing each tool and checking the screen and the written cells.

window.__tedixWebMcp.status() in the page lists the registered tools and whether the browser offers a WebMCP host.

Activity feed

tedix:app/activity-feed reads Work changes, settled lane and tedi results, the viewer’s decisions and approvals, and personal corrections. Configure version: 1 and projects: [{id, label}]. Optional factorySources reuse Card review sources as {projectId, source} to read existing factory sheets. They read the latest configured runs, with their output revision and row as evidence. The feed never runs a sheet’s action.

pageSize, attentionBudget, digestBelow, quietHours (start/end hours in the viewer’s timezone), weights and deckUrl control presentation. Work, result and inbox readers have independent cursors; personal ranking applies to loaded records. Low-importance updates form an expandable digest. Pending decisions and critical events remain visible during quiet hours.

Reactions record personal learning events. With decision learning enabled, More like this and Not for me adjust the next ranking from those events; otherwise they save personal preferences in the existing OS preference store. Comment records a correction for the existing reflection process. Turn into Work creates proposed Work with its source link. The feed does not approve decisions, send messages or change admission. Its WebMCP tools read visible cards and use the same personal reaction callbacks.

Learn from decisions

An administrator can enable decisionLearning in organization settings with configure_decision_learning. Deck and Feed then use the same learning score. A person’s choices stay personal; shared signals and approved lessons provide team context. Deadlines and waiting costs change the score as time passes. Repeated deferrals and negative reactions reduce a class, source or proposer’s weight; quick responses raise the weight of a project.

Open Why this? to see the signals and applicable lessons behind the rank. After a decision, the learning line says what changed. Undo learning records a correction and recalculates the model; reversing the original action uses that action’s own controls.

unchangedThreshold defaults to five; autoApproveClasses explicitly selects eligible reply classes. A rejection resets the counter. auditSampleRate keeps a stable sample for review. Automatic replies still require the existing reply policy, reversibility checks, delivery checks and session budget. A learned preference never grants permission to send an email or perform another protected action. The stored reply receipt names the supporting signals.

halfLifeDays, fastDecisionMs, minimumPreferenceExamples, shorteningRatio, deadlineWindowHours, timezone, quietHours and weights tune the model. classifyEdits, classifierConfidence, classifierTimeoutMs and preferenceModel control bounded edit interpretation. A failed classifier leaves the measured edit intact. Outbound review, reply drafting and Home writing read learned preferences before generating text. The Outbound style profile includes the learned writing preferences.

Add another built-in app

Put the settings schema in packages/api-contract/src/schemas/ and check it in OsGadgetManifestWriteSchema, put the engine in apps/os/src/lib/ with unit tests, register its entry in workspace-app-renderer.tsx, and build the screen from @/components/kumo/*. Read and write through the existing OS queries and the generated osQuery keys. Expose its actions as WebMCP tools as described above.

Traps

  • Tenant choices belong in config. Hardcoding a workspace, sheet id or column in code breaks the next tenant.
  • Reads stay lenient: only writes check config, so an old revision never becomes unreadable after the schema changes.
  • A range write must fit the sheet’s current columns. Write one rectangle and copy the current values of any column between the ones you change.
  • Write verdicts with expectedRevision and retry once on a conflict; a factory may add a sheet or row at the same time.
  • In chat, a bare follow-up such as “mark it useful: great angle” right after Home showed a card writes that card’s row directly, without a delegation: Home takes the card named at the top of its previous answer and the sheet that answer cites by output id or Sheet: title line. When the answer names neither, name the sheet in the request.
  • Never claim an action happened because the done column says yes. It records a decision; the provider’s own record is the proof.

Work: Feed and Deck

Work opens the installed Feed app unless a Work app declares navigation.workDefault: true. Decide opens the installed Deck and shows its current personal inbox count, including Work and runtime approvals. A + means another page may contain more choices. The phone bar has only Feed and Deck; there is no Work tab bar or More menu.

Set navigation: {"work": "feed"} or {"work": "deck"} on the corresponding app manifest to choose its Work role. Existing Feed and Deck apps opted into navigation.board also work. Discovery uses the viewer’s accessible workspaces; installing an app never grants access. If several apps declare the same role, explicit role links select the oldest active app, with its id breaking creation-time ties. Other apps opted into navigation.board appear under Apps in the Feed.

To start in Deck, set navigation: {"work": "deck", "workDefault": true} on its installed manifest. An explicit view or app in the URL still wins. If several accessible Work apps declare the default, the same oldest-app order breaks ties. An app without a Work role cannot become the Work start app.

The Feed header summarizes choices due today, sessions currently reporting work in its projects, and the earliest known unresolved deadline. This is reported activity, not human online presence. Counts and deadlines describe loaded records; unread sources stay visibly unavailable.

Old collection URL Destination
/work/attention /work?view=feed&filter=attention — Needs action
/work/needs-you /work?view=deck
/work/queue /work?view=feed&filter=queue — In progress
/work/queue?disposition=completed, /work/attempts, /work/history /work?view=feed&filter=history — Done
/work/recovery, /work/stalled /work?view=feed&filter=stalled
/work/portfolio /work?app=portfolio — installed Portfolio app
/work/agents, /work/office, /work/outbound The matching installed app

Work item, project, request, run and receipt links remain direct. Office keeps the personal request ledger and Support section. The existing Attention app can use config: {"scope": "personal"} to show personal session activity and request filing controls; omission keeps it scoped to its containing workspace.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close