101 Commits
Author SHA1 Message Date
Abue Ammar 4935e937b0 Install extensions from GitHub; search only the Raycast Store (#1262)
* Install extensions from GitHub; search only the Raycast Store

Search now queries the Raycast Store alone: no GitHub search, no merged
results, and no registries or store toggle. A new "Install from GitHub"
row builds one extension from a repository or folder link with the
chosen package manager, keeps only the build, and deletes the source and
dependencies with the install's workspace. Closing the panel cancels a
running build.

* Refactor ExtensionStorePanel layout and improve StoreRow details

* Check store extensions for updates when Settings opens

Record the store's commit for each store install, and an unknown version
for each Raycast import that the first check adopts. Opening Settings ›
Extensions looks each tracked extension up by handle and name and offers
an Update per row and an Update All above the list. Folder and GitHub
installs are never checked. Uninstall moves out of the expanded settings
to a trash icon on the row, with Update to its left.

* Stop a cancelled install's whole process group and release its timeout
2026-10-02 01:43:47 +06:00
Sven Jacobs 37daebac16 Add window management shortcut presets (#1261)
* Add window management shortcut presets

Settings › Window Management can now fill in the stock window shortcuts of
Rectangle / Magnet or Spectacle: pick one from the pop-up and click Apply.
Rectangle adopted Magnet's scheme wholesale, so the two share one entry.

A preset only fills in its own commands; every other shortcut stays as it
is. It asks for confirmation only when it would replace a shortcut the user
set, and never takes a key held by another action. Bindings are stored as
physical key codes, as the original apps register them, so non-QWERTY
layouts land on the same keys. Apply stays disabled while the chosen preset
is already fully applied.

Closes #730

* Re-plan a preset after its confirmation

The confirmation dialog suspends apply, and a settings.json reload can
change window bindings meanwhile. Rebuild the plan from the live bindings
once the user confirms, and ask again if it would now replace a shortcut
the dialog didn't list.
2026-10-01 23:42:33 +06:00
notsoshu 9f8bb1f738 Show leading think blocks from OpenAI-compatible providers as reasoning (#1236) 2026-09-30 12:43:38 +06:00
notsoshu a008987d29 Render LaTeX math in AI chat and Quick AI replies (#1238)
* Render LaTeX math in AI chat and Quick AI replies

Replies now typeset inline \(...\) and $...$ and display \[...\] and
$$...$$ math natively, in STIX Two Math through its OpenType MATH table,
with no dependency. Each formula is one attachment character in the
reply's single text view, so selection stays continuous, find and
citations keep their offsets, and copying gives back the LaTeX source.

Prices like "$5 and $10", escaped dollars and code spans stay prose.
An equation still streaming in, or one outside the supported subset,
shows as its source; a display equation outside it shows as a LaTeX
code block. A formula wider than a narrow Quick AI column scales down.

* Hold back an equation until it finishes streaming

A display equation still arriving showed as its source in a left-aligned
paragraph, then jumped to the centre once its closing delimiter landed.
Now, while a reply streams, a display equation at the very end is a
muted placeholder where the equation will sit, and an inline \( or \[
still open at the end is withheld until it closes.

Only the tail of text still arriving is touched: the last segment of a
streaming chat reply, and a Summarize result while it runs. An opener
the stream has moved past, or one in a finished reply, still shows as
source. A lone $ is never held back, since it may be a price. Find
parses the same way, so its matches stay on the drawn words.

* Document LaTeX math in AI replies

* Keep an equation held back when the stream has just sent a newline

A reply streaming "$$\nx = ...\n" ends on an empty line, which the
reader took for a blank line inside the equation and so for proof the
opener was stray: the half-written equation showed as source until its
closing $$ arrived. Mid-stream, a blank line now counts only once text
follows it, for display equations and for an inline one at the end.

* Expose each equation to VoiceOver as an image

The attachment's accessibility element carried the LaTeX as its label
but reached assistive apps with the role AXUnknown. It is now an image,
so VoiceOver meets it as one element named by its source.
2026-09-30 12:41:11 +06:00
Abue Ammar 22f7ea8e98 Let a snippet take its own global shortcut (#1229)
Each snippet's Settings row records a shortcut that runs the same expansion
funnel as its launcher row. Typed text now clears held modifiers, so the
shortcut's own keys no longer ride along on every character.
2026-09-30 02:52:34 +06:00
Abue Ammar e14a3ba36e Paste history one entry at a time with Paste Sequentially (#1221)
* Paste text history one entry at a time with Paste Sequentially

Paste Sequentially is a new clipboard command. Bound to a shortcut, each
press pastes the next older text entry into the app in front, without
the palette. Copy A, B and C, and three presses paste C, B and A.

The walk takes its entries' ids at the first press and never promotes,
so it cannot repeat or skip an entry, and an entry deleted mid-walk is
skipped rather than pasted. A copy made since the last press, or a
minute without one, starts it over from the newest entry. Past the
oldest entry it says so instead of wrapping.

Each press drains the poller before it writes, so a copy made inside
the 0.5s poll interval reaches history instead of being overwritten.

* Hold back a Paste Sequentially press that comes before the last paste lands

A press writes the next entry to the pasteboard at once and posts ⌘V
50ms later, and the target reads the pasteboard only when it handles
that ⌘V. A second press inside that gap swapped the pasteboard first,
so one entry was pasted twice and another skipped.

A press within 0.25s of the last paste is now dropped. It is not
queued, since a queue would replay a held shortcut as a burst of
pastes. A dropped press does not move the walk, so nothing is skipped.

* Paste images and files with Paste Sequentially, not only text

The walk now takes every entry in history, newest first, so a run of
copies that mixes text and images pastes all of them in order. Paster
already writes each kind, so the filter was the only thing in the way.

An image or file gone from disk writes nothing. The same press now
moves on to the next entry, where before it did nothing at all.
2026-09-30 02:35:02 +06:00
Abue Ammar 6fc6aa1b90 Stop child processes from hanging their caller after they exit (#1188)
`waitUntilExit()` spins the calling thread's run loop, and on a GCD thread it
can miss the exit and block forever. About 3% of an extension's `execFile`
calls never settled, each holding a thread and two pipes until quit.

`Process.runObservingExit()` sets the termination handler before `run()` and
returns a `ProcessExit` that waits on a semaphore instead. Every
`waitUntilExit()` call site now uses it.
2026-09-27 02:09:09 +06:00
Abue Ammar 1d7d22663f Add an opt-in settings.json mirror, and custom Snippets and Notes folders (#1186)
* Add an opt-in settings.json mirror for preferences and window management

* Let Snippets and Notes use a chosen folder, from Settings or settings.json

* Keep a Notes or Snippets folder change from reusing the old folder's contents

* format fix
2026-09-27 01:51:22 +06:00
Abue Ammar a300cfecd3 Walk into a project's windows with Rooms (#1155)
* feat: Implement Room Management feature with UI components for room selection and preview

- Added RoomPickerList and RoomPickerScreen for selecting and managing room windows.
- Introduced RoomPreviewController and RoomPreviewView for displaying room previews.
- Created RoomsList and RoomsScreen for switching between rooms and managing layouts.
- Documented the Rooms feature, including layout options and usage instructions.

* Drop Show All Windows from rooms

Parked windows still come home on quit, when Window Management turns off
and at launch after a crash; entering another room returns any parked
window whose app it hides. The command, its Actions row, the Settings
button and its search entry go, with the docs that named them.

* refactor: Improve code readability by formatting return statement and frame modifier

* Address review on Rooms and trim comments

- Remember Arrangement no longer duplicates members.
- A bad stored room is dropped alone; an unknown layout resets to Auto.
- Switching the feature off unhides only the apps rooms hid.
- Settings reaches RoomCoordinator through the environment.
- The preview honours Reduce Motion on hide and card transitions.
- Window Switcher disarms a stale release monitor when it opens.
- Remove comments that restate the code.
2026-09-25 16:45:17 +06:00
4533c15d1e Copy the text in a clipboard image with ⇧⌘T (#1030) (#1147)
* Copy the text in a clipboard image with ⇧⌘T (#1030)

An image in clipboard history can carry text nothing could reach: both
the ⇧⌘T chord and the Actions-menu row now offer Copy Text on it.
Eligibility is ClipboardItem.offersTextExtraction — a captured image
blob, or an image file copied in Finder — never a text entry and never a
PDF, which stays a background-indexing capability. The action closes the
palette, stats the file so a vanished row raises the HUD its kind
already uses, then shows a "Reading text…" pill while ClipboardTextWorker
spawns the bundled helper: no Vision runs in the app process, nothing
reads the item_text table, and nothing depends on the text-search
switch. The extracted text lands on the pasteboard unmarked, so the
copy enters history like Copy Path, and the pill is replaced by its
outcome — Copied text, No text found, or Couldn't read the text.

The clipboard and palette-shortcut harnesses cover the eligibility
answer and the new chord, and every harness that compiles
ClipboardStore now compiles ClipboardFileKind beside it.

* Read Copy Text screenshots in whole lines, once each

OCR tiled a bitmap into overlapping 2048-pixel squares, which was
harmless for search but wrong for a copy: a Retina screenshot's lines
came back cut at the tile edge with their tails appended at the end,
and a tall phone screenshot repeated every line in the overlap band.
Recognition now runs on full-width strips, and each strip keeps only
the lines centred in its half of an overlap, so every line is read
once, whole and in order. The pixel budget is unchanged: a larger one
measured worse, since Vision downsamples a wide input itself.

Copy Text stats the source off the main actor after the progress pill
shows, a newer trigger cancels the helper an older one is waiting on,
and a result never overwrites a copy made while the helper ran.

---------

Co-authored-by: mch <omitted@email.com>
Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-25 14:07:51 +06:00
Federico Zivolo f2a27d7d5a Keep SwiftUI's named keys out of the ASCII recovery (#1106)
* Keep SwiftUI's named keys out of the ASCII recovery

* Cover the shortcut caller path, not just the recovery

* Skip the caller-path cases when the layout has no trap to spring

* Keep the recovery's contract in Platform, without the test seam
2026-09-24 23:41:32 +06:00
Ruben Nogueiraandabue-ammar 75974f86fb Add an AI Chat window with a conversations sidebar (#1047)
* feat(ai): add an AI Chat window with a conversations sidebar

A Raycast-style AI Chat window: saved conversations on the left, the open
one on the right, and a composer that picks the model, reasoning effort and
tools per chat. The palette's chat becomes Quick AI; ⌘J moves a Quick AI
conversation into the window.

- Each chat keeps its model, effort and tool scope; reopening it restores them
- Reasoning shows as collapsible thinking blocks, one per stretch of thinking
- Image paste and attachments for vision-capable models (Claude, Codex)
- Model lists come from each CLI's own catalog, so new Claude models appear
- Context gauge with a hover card: history, budget, files, tools, tokens
- Choices render as buttons above the composer; a bare "choices" list counts
- Sources list with citation numbers at the end of the citing sentence
- Find in Chat (⌘F) steps word by word and marks each match in place
- A reply's text is one selectable NSTextView, so a drag spans paragraphs,
  lists, code and tables
- Chats are titled by their harness as soon as the first question is sent
- Several Codex chats can answer at once, each on its own thread
- Actions menu, pin, rename, copy and delete from the sidebar

* Refactor AI chat components and enhance functionality

- Updated `ChatTitle` to improve string sanitization.
- Modified `InstalledAI` to enhance name extraction logic.
- Renamed database tables from `conversation_meta` to `conversation_details` and `message_meta` to `message_details` for clarity.
- Added export functionality for chat transcripts in `AIChatCoordinator`.
- Improved UI elements in `AIChatDetailView` and `AIChatSidebarView` for better user experience.
- Enhanced `AIChatWindowChrome` to streamline toolbar actions and improve navigation.
- Updated keyboard shortcuts for new chat, settings, and other actions across various components.
- Improved chat transcript rendering in `ChatMarkdownText` and `ChatTranscriptView`.
- Enhanced `QuickAICoordinator` to support regeneration of responses.
- Updated documentation to reflect changes in database structure and new features.

* feat(ai): enhance AI chat sidebar and history functionality with new continue action

* feat(ai): update AI chat window and sidebar dimensions for improved layout

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-23 23:36:31 +06:00
notsoshuandabue-ammar e307213c23 Offer Tinycast's MCP servers on the Codex and Claude routes (#1081)
* Offer Tinycast's MCP servers on the Codex and Claude routes

* Give Codex's local servers their own variable names, and run each once

Codex forwards a variable only under the name it already has, so a local
server read TC_MCP_<HANDLE>_<KEY> instead of its own key. A server with
variables now starts through /bin/sh, which moves each value to the name
the server reads and execs it; the script carries names, never values.

While Codex or Claude is the chat model, Tinycast keeps no connection of
its own to a local server, since the CLI starts its own copy. AppCore
re-applies that whenever the chat model changes.

* Name Codex's MCP variables by position, so no two secrets share one

The derived TC_MCP_<HANDLE>_<KEY> upper-cased and flattened both halves,
so github-x + TOKEN and github + X_TOKEN, token and TOKEN, a remote
header and a local key, or two non-Latin handles of one length all met
in one variable, and one server received another's secret. Each value
now lives under TC_MCP_<server>_<key>, positions in the launch's own
list, and a name that repeats anyway refuses the launch.

A key the shell cannot export is no longer forwarded under a name its
server never reads.

* Ask before every Claude MCP call, whatever the reader's settings allow

The armed Claude turn relied on the CLI's own permission system to raise
can_use_tool, and the reader's settings could answer first: an allow rule
for their own server of the same name, or a bypassPermissions default,
ran the tool with Ask Each Chat never asked. The turn now pins
--permission-mode default and passes --settings with a permissions.ask
rule for every armed server, which outranks an allow rule from any
source while leaving the rest of the reader's settings in force.

* Give Tinycast's servers their own names on Codex, and disable all of yours

-c sets single keys, so a Tinycast server named like one of the reader's
own Codex servers merged into it: a local one inherited their env table,
literal secrets included, their cwd and any per-tool approval_mode that
skips consent, and a remote one over their stdio one made Codex refuse
the whole config. Tinycast's servers now go by tinycast-<handle>, every
one of the reader's is disabled with no exception, and a launch whose
armed name the reader already uses is refused. Elicitations and tool-call
items map back to a handle only through that prefix, so a question about
any other server is declined without asking.

* Refuse to start Codex when the reader's MCP servers cannot be kept out

A failed or unparseable `codex mcp list --json` launched the app-server
with nothing disabled, so every server in the reader's Codex config
started inside the Tinycast thread: the boundary failed open. It now
fails closed with an error that says why, and so does a reader's server
whose name holds a dot or an equals sign, which `-c` splits and so
cannot switch off. Names with spaces or non-Latin letters are
addressable and stay as they were.

* Launch the Codex app-server once for concurrent starts

A status refresh racing a turn, or two quick sends, each passed the
isRunning check, read the list and launched; the second overwrote the
first's process and pipe, and when the first exited its handler tore
down the live one and failed the turn. A launch is now one shared Task
that every caller for the same list awaits, handshake included, a stop
that lands while the list is read keeps the launch from starting after
it, and an exit is acted on only when it is the current process's.

* Cover the Codex relaunch on a changed server list

Two turns with the same list run in one app-server; a third whose
server carries a different secret, as a refreshed token would, starts
a second one with the new value in its environment.

* Ask about one CLI tool call at a time

Each Codex elicitation and each Claude can_use_tool is answered on its
own Task, and DialogController refuses a second dialog while one is up,
so a call arriving during the first question was told the reader had
declined it. AIToolServerSession now asks one question at a time, in
arrival order; the next is decided only after the dialog before it
closes, so it sees the grant that dialog made. Questions still waiting
when their turn ends are cancelled rather than asked.

* Offer credential-free HTTP servers to the CLIs, and lend tokens as Authorization

MCPServer.toolServer dropped any HTTP server without a header value, so
one that needs no credential worked on the API route and was silently
missing on Codex and Claude. And a lent OAuth token went out under the
stored header name, so a server switched to OAuth from X-Api-Key sent
"X-Api-Key: Bearer <token>" and got a 401. Only an OAuth server with no
session is left out now; a Header server with an empty value is offered
with no header, which both encoders omit, and a lent token always goes
as Authorization, as Tinycast's own transport sends it.

* Delete a crashed turn's private files at the next launch

A Claude turn's MCP configuration, which carries the servers' secrets,
and Grok's prompt file were removed only when the turn ended, so a crash
mid-turn left them in the workspace indefinitely. InstalledAIManager now
removes any tinycast-mcp-*.json and tinycast-prompt-*.txt older than the
launch when it starts.

* Stop the Codex helper when a server it runs is withdrawn

The helper re-reads its server list only on the next turn or after ten
idle minutes, so switching MCP off, removing a server, setting it to
Never Allow or signing out of it left that server's process, and any
token lent to it, running inside the helper until then. MCPCoordinator
now tells ChatGPTSubscriptionManager which servers are still offered,
and a helper launched with any other stops if no turn is running.

* Route a Claude tool name at its first separator

ClaudeMCPLaunch.route split mcp__<handle>__<tool> at the last "__" on
the premise that a handle may hold one. It cannot: MCPSlug emits only
letters, digits and "-". A tool name can, so mcp__files__read__file
routed to a handle "files__read", permit found no server, and the call
was refused without a question.

* Escape every control character in Codex's TOML strings

quoted() escaped only backslash, quote, \n, \r and \t, matching each as a
Swift Character, and CRLF is one Character, so it matched neither and
went through raw; so did every other control character. Codex then
refused the whole config. Each Unicode scalar is now considered, and
everything below U+0020 and U+007F goes out as \uXXXX, which tomllib and
the real codex both read back as the original.

* Refresh an OAuth token before lending it when under ten minutes remain

A token lent to Codex or Claude had only to last 60 seconds past the
turn's start, since that is when Tinycast's own requests refresh, but the
CLI holds it for the whole turn and cannot ask for another. Lending now
refreshes within ten minutes of expiry; a token with no refresh token,
or whose refresh cannot be served, is lent as it is.

* Name a Codex consent question after its own call

The runner named the tool in an elicitation from the latest mcpToolCall
item started on that server, which is another call whenever two run at
once. CodexElicitation now keeps _meta.tool_name, and the question uses
it whenever Codex sends one. A stub turn with two calls started and
asked about together pins both names, and that the second question
waits for the first.

* Answer a Claude control request that is not a tool question

A control_request whose subtype Tinycast does not know, or a
can_use_tool for a tool on none of its servers, decoded to nothing and
was never answered, so the CLI waited on it for the rest of the turn.
Each now gets the SDK's error control_response.

* Create Claude's per-turn MCP file private from the start

FileManager.createFile writes its data to a temporary file at the
default 0644 and applies the 0600 attribute afterwards, so the file
carrying the servers' secrets was briefly readable by other accounts;
only the 0700 workspace stood in the way. It is now opened with
O_CREAT | O_EXCL at 0600 and written through that descriptor.

* Keep each comment the CLI routes added to one line

The CLI-routes change added 32 runs of two or more comment lines and
several over the 100-character cap. Each is now one line, and the
reasoning they carried lives in docs/features/mcp.md: why the listing
and the launch share their flags, why a check keeps the running list,
why the Claude file is per turn, and what the consent channel is. That
paragraph also stops overstating the --allowedTools fallback: an allow
list denies nothing by itself, --permission-mode dontAsk does.

* Say which servers a CLI route is not handed, and what argv can carry

The MCP doc promised the same server list on every route and that no
secret reaches argv. An OAuth server nobody is signed into is left out
on Codex and Claude, and a credential typed into a server's URL is part
of the URL, which Codex takes as a launch argument; both are now said.

* Tell readers what is different about MCP on Codex

The website said the servers on the CLI routes are the same as
everywhere else and that only adding, removing or re-authorizing a
server restarts Codex's helper. It now also says that an OAuth server
nobody is signed into is left out, that moving between an @handle
message and an unaddressed one restarts the helper too, that
withdrawing a server stops the helper, and which environment variable
names reach a server Codex starts.

* Run Codex and Claude with no round cap on Unlimited

The CLI routes took their cap from AIToolRounds.rawValue, which is -1
for Unlimited: Claude would have been passed --max-turns -1, and Codex
interrupted at its first call saying it stopped after -1 rounds.
AIToolServerSession.rounds is now optional and comes from
toolRounds.limit. On Unlimited, Claude is given no --max-turns, since it
has no cap without one, and Codex counts no calls, so only the model or
Stop ends the turn. A step still stops both, and the sentence names it.

A turn with no tool servers stays a separate case. Claude with nothing
to call still passes --max-turns 1, and the sentence for a cap Claude
reports now names the number it was actually given; a max-turns result
under no cap says Claude could not finish the response instead of
naming one Tinycast never set. Codex with no session keeps its cap of
one call.

* Let the two-call consent test accept either order of questions

* Tell readers that Codex reads a server's resources without asking

* Read every forwarded value before exporting any in the Codex MCP shim

* Let a Codex status check join a turn's pending launch instead of relaunching without its servers

* Refactor code for improved readability and maintainability

- Adjusted formatting in various Swift files to enhance code clarity.
- Updated MCP OAuth handling to improve error messaging and flow.
- Enhanced UI components for better user experience in settings and chat transcripts.
- Improved handling of asynchronous tasks in CodexAppServerClient and InstalledCLITurnRunner.
- Refined documentation for AI features and MCP integration.

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-23 19:32:32 +06:00
notsoshu 3463f33299 Add OAuth authentication for HTTP MCP servers (#1077)
* Add OAuth authentication for HTTP MCP servers

* Escape a plus sign in the OAuth authorization URL

* Drop an issuer's terminating slash before inserting its well-known path

* Keep the issuer comment under the 100-character cap

* Keep a saved server signed in when Test Connection tries an edited URL

* Let a token refresh finish when the server editor closes or saves

* Say why an MCP server was kept when Remove cannot delete its credentials

* Cover supplied OAuth client credentials in the harness
2026-09-23 18:45:12 +06:00
Abue Ammar d93a09baac Rebuild root search ranking and add Suggestions (#1057)
* Rebuild root search ranking and add Suggestions

Root search priced each match as a cell of a role-by-tier table plus a
boost learned per submitted query, and indexed Spotlight's alternate
names, which merge every language a bundle ships. On an English Mac `ll`
and `sap` found Safari, `settings` ranked Tinycast's own command above
System Settings, and loose subsequence hits crowded the list.

Each field now gets an alignment score that rewards word starts, Search
sensitivity decides how loose a hit may be and still show, and an
ordered comparator settles two entries by exact hits, past search
terms, match score, frecency and kind. Learning is one frecency record
per entry: an open adds 100 to a score that halves every ten days. With
the field empty, a Suggestions section offers fresh installs, what the
user opens most and a few built-in commands; Show suggestions turns it
off.

The old learned table does not decode, so learned ranking starts empty.

* Enhance fuzz-test with a seeded random number generator for reproducibility; update ExtensionCatalog and ExtensionManager to include installation date; refine AppIndex to track usage order and improve launcher documentation.
2026-09-23 10:16:06 +06:00
Clément Knodererandabue-ammar c3a54c71ac Add Globe hotkey bindings (#1010)
* Add Globe hotkey bindings

* Fix Globe hotkey monitoring and cancellation

* Label hotkey permission warning for accessibility

* Drop Input Monitoring from modifier-only hotkeys

* Unify modifier-only bindings behind one trigger map

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-23 02:39:54 +06:00
Jonas List a27fd41638 Run an extension's menu bar command, and draw its item natively (#963)
* Prepare extension runtime for menu bar commands

* Host Raycast menu bar commands with transient runtimes

* Stabilize extension menu layout and lifecycle

* Restore menu dismissal and release runtime temporaries

* Preserve pending menu actions and explicit launches

* Render menu actions before loading icons

* Promote menu interactions and retry returning icons

* Keep cached menu actions responsive during reload

* Prepare extension menus before opening from bottom edge

* Match native status menu spacing on every display

* Release extension HTTP sessions and reuse private transport

* Record extension HTTP retention investigation and validation

* Keep background refreshes out of the foreground runtime

Rebasing onto main brought in scheduled no-view refresh, which shares the one
JSContext with the palette. Four ways that went wrong:

- A refresh that finished before its waiter registered recorded a timeout, so a
  fast command backed its own schedule off as though it had failed.
- Boot and bundle reads suspend. A foreground launch in that gap aborted the
  refresh, but the refresh then started inside the context that replaced it.
- Disabling extensions left a running "Refresh Now" alive, and a queued one could
  still start afterwards.
- A scheduled command could not launch a sibling without naming its extension,
  since ownership resolved only from the foreground session.

ExtensionBackgroundSession now owns one run: the outcome is buffered rather than
signalled, and the first writer wins, so neither an early finish nor a late
callback can be lost or overwritten. A preempted run is cancelled rather than
recorded, leaving its schedule where it found it, and every suspension point
rechecks session identity before touching the runtime.

Also drops the duplicate ExtensionLaunchType the merge left in ExtensionBootConfig.

All 64 harnesses pass; Debug builds with no new warnings. Verified against the
real OpenCodex Usage and Port Manager menu commands.

* Let AppKit own the extension status menu

Clicking a second extension menu bar item while one was open only closed the first: the button drove a manual popUp, whose modal tracking loop swallowed the click instead of handing it to the other item. Two native menus hand off, and ours did not.

Attach the menu to the status item and let AppKit position and track it, which restores that handoff along with Escape, outside clicks and click-to-close. The manual anchor maths and its two spacing constants go with it, since AppKit places the menu itself.

The menu is attached once at init rather than per snapshot, so the first click opens it before any render has arrived.

* Key menu icons by value instead of scanning for them

A menu rebuilds every row on each React commit, and each row searched two arrays for its icon and a third for the failures. Small menus make that cheap, but it is linear work on a path that runs several times a second while a menu is open.

RenderValue and RenderNode gain Hashable, so the cache becomes a dictionary and the failure list a Set. Same behaviour, no per-row scans.

Also restores the guard that a menu bar extra with no rows detaches its menu, which the switch to AppKit tracking had dropped: without it an extension rendering nothing would open an empty menu.

* Cut every menu bar comment back to one line

* Drop the menu memory investigation log and table the shortcut keys

The benchmark file recorded how one HTTP retention bug was found, which the feature doc already states as a rule. No other investigation is kept this way, so it goes rather than starting a convention.

The named-key switch becomes the table it always was.

* Reuse the existing refresh, metadata and icon paths for menu bar commands

The branch was written against an older main and grew its own copies of
machinery main already ships. Menu-bar activation and the saved button now
live on the command's own ExtensionCommandMetadata record, so
ExtensionMenuBarStore and extension-menu-bars.json go away and the writes
coalesce on the metadata store's debounce instead of hitting the disk on
every React commit. Menu-bar refresh cadence comes from
ExtensionRefreshPolicy.nextDue, which adds the failure backoff and the
per-command phase the hand-rolled scan never had.

ExtensionBackgroundSession and its rewrite of the manager's background
internals are gone: main's continuation path does the same work, so the
scheduler is main's again. Manifest intervals parse once through
ExtensionRefreshPolicy.parse, which takes a floor and now rejects an
amount that overflows to infinity. Menu-bar icons load through
ExtensionImage.load rather than restating its four bitmap cases, which
also fixes fileIcon to draw fitted like every other icon here.

Settings reads the manager directly, mirroring the background-refresh row,
so the coordinator no longer threads through four views.

* Reach the menu bar toggle through the extension coordinator

A feature action belongs on its coordinator, with AppCore only locating it. The
Show in menu bar toggle read and wrote ExtensionManager directly, past the
coordinator the enable switch two hundred lines above already goes through.
2026-09-21 01:17:49 +06:00
Jonas List d721ca3d44 Draw the oklch() swatches Color Picker states its colours in (#917)
Organize Colors builds each tile as a bare {color} swatch whose string comes
from getPreviewColor(), which formats in oklch() while the row label keeps the
user's hex preference. The extensions layer carried its own colour parser that
read #rrggbb and nothing else, so every swatch resolved to nil and fell through
to the icon placeholder: correct hex beside a grid of grey boxes.

ColorValue is already the one CSS parser, and ColorSpaces already held the
Oklab matrices one way round, so the inverse initialiser lands there and
parseFunctional gains an oklch case that reuses its own argument, percentage
and angle helpers. ExtensionImage now calls that parser and drops its hex
reader, which also gives extension tints rgb() and hsl() for free, and lets the
clipboard read back the oklch() it could already write.
2026-09-19 19:41:13 +06:00
Jonas Listandabue-ammar 00516759c4 Hand an app-picker preference to an extension as an Application (#907)
* Hand an app-picker preference to an extension as an Application

An appPicker preference reached JS as the bare path it is stored as, but
Raycast hands one over as an Application — { name, path, bundleId }. Project
Manager reads vscodeApp?.name.replace(...) at module scope, so the command
threw before its first render.

ExtensionPreferenceValue gained an application case whose JSON form is that
object, resolved from the bundle at the path — the shape
ExtensionHostBridge.describe(application:) already sends for getApplications().
ExtensionPreferenceSchema.runtimeValue converts app pickers only, and returns
nothing for an empty path, so an unset picker arrives as an absent key and the
extension's own optional chain short-circuits instead of throwing. Storage is
untouched: the picker still writes a path, so Settings, backups and the
required-preference check read what they always did.

* Cover an app-picker preference surviving a storage reload

Also applies swift-format to the new ext-test checks.

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-19 05:26:51 +06:00
Jonas List ca3da2ee4e Add the WebSocket and mDNS support Home Assistant needs (#901)
* Run extensions that speak WebSocket

A bundled `ws` handshakes through `http.request` and wants a raw socket back,
which the fetch-backed shim had none of. Sockets are `URLSessionWebSocketTask`
now, and the upgrade path hands `ws` one that re-frames RFC 6455 both ways.
Home Assistant is the reference case.

* Resolve a .local host without joining a multicast group

Home Assistant's default `homeassistant.local` is resolved by the extension
itself with multicast-dns, which died on `dgram.createSocket`. `dgram` now
answers an address query out of `getaddrinfo` — mDNSResponder handles
`.local` — so nothing multicasts and no entitlement is needed.

* Harden the WebSocket and dgram shims

A rejected host call poisoned the send queue, so every later send and the
close after it rejected too; the queue now recovers while the caller still
sees the failure. A ping is answered by the peer through
`URLSessionWebSocketTask`, not by a pong the adapter invents. An upgrade no
listener claims destroys the socket, the way Node does, so the native task
goes with it. `dgram` answers address questions only. And a socket id that
is not a whole number falls back instead of trapping.
2026-09-19 03:40:08 +06:00
Abue Ammar eb32d8fe4a Collect a custom command's arguments inline in root search (#903)
* Collect a custom command's arguments inline in root search

A custom command that declares arguments now shows its fields beside the
search field when its row is selected, the way a quicklink already does,
instead of replacing the screen with a one-at-a-time form. The form,
`PaletteMode.customCommandArguments` and `CustomCommandArgumentSession`
are gone.

Handled the way Raycast handles script-command arguments:

- At most three. `CustomCommandArgument.sanitized` enforces it on every
  path in, so a stored command carrying more keeps its first three; the
  editor's Add stops there.
- ↵ with a required field empty focuses that field rather than running.
- A hotkey, favorite slot or Run Again with values missing opens root
  search onto that one row, seeded with its name, first empty field
  focused. The row is listed even when hidden from the launcher, since
  its shortcut still has to be answered.

Fields are keyed by position (`$1`–`$3`), not name, because two arguments
may share a name. `runCustomCommand(id:values:)` stays the one funnel and
`positionalValues(from:)` restores `$n` order, so values still reach zsh
as positional parameters and never as command text.

The field strip moves from Quicklinks to `DesignSystem/InlineArgumentFields`
so both features draw the same control rather than a copy of it.

* Carry a clicked row's inline values into its launch

Clicking a launcher row launched it without the values typed into its
inline fields, so a custom command reopened its prompt empty and a
quicklink lost what was typed. The click now goes through
`argumentValues(for:)`, as ↵ already did.

Also names what `argumentKey`'s second half is for each feature.
2026-09-19 03:34:37 +06:00
huhrishabh 141a7a918e Let raycast-width and raycast-height shrink Detail markdown images (#891) 2026-09-18 22:20:46 +06:00
Ales M 9a9d8d2a9b Render Markdown in Notes (#880)
* Parse Markdown notes into ranged lines

* Add the Notes Markdown edit planner

NoteMarkdownEditing turns a NoteEditAction into a single NoteEditPlan: one
range, one replacement, one resulting selection. Every list, indent and
inline-style gesture resolves through it, so each is one undo step and none
of it needs a text view to test.

NoteRevealPolicy decides which lines show their raw syntax for a given
selection, widened to whole fenced blocks.

* Render Markdown in the Notes editor

* Edit Markdown notes with shortcuts, tasks and links

* Add a Render Markdown setting to Notes

* Document Markdown rendering in Notes

* Add code block and quote edit planning

Extends NoteMarkdownEditing with fenced code and block quote toggles.

NoteFormatting is the report the caret's context produces, decided by the
same span and line rules the toggles use, so a lit button always undoes.

* Add code block and quote shortcuts to Notes

* Add a formatting bar to Notes

* Simplify the Notes Markdown engine

Scan inline spans on demand instead of storing them on every parsed line:
only the styler, the editing rules and NoteTitle read them, and each reads
one line at a time. NoteMarkdown keeps its UTF-16 units and vends
inlines(of:) on request.

Adopt NSTextStorageDelegate in NoteMarkdownRenderer, which reports the
edited range and length delta for every mutation including undo and marked
text. That replaces a full shadow copy of the document and the prefix and
suffix diff run against it on every edit, selection change and read.

Emit the trailing empty line as a real zero-length line when the source
ends in a terminator, so the caret after a final newline sits on a line
like any other. Five restatements of that special case go away.

Delete NoteTask, superseded by NoteMarkdownParser and no longer referenced.

At 100,000 characters typing drops from 6.8 to 5.6 ms at the end of the
note, 5.2 to 4.0 in the middle and 2.8 to 1.6 at the start.

* Move the Notes formatting bar to the trailing edge

Mirror the band under the editor: the character count leads, the formatting
capsule trails. The expand transition anchors trailing so the buttons grow
out of the round button leftwards, and the tooltip alignments swap with it.

Anchor the heading menu to the heading button's own frame, reported by the
laid-out view, rather than re-deriving the capsule's geometry in AppKit.
The old anchor guessed from the window edge and a capsule height composed
from two tokens, which put the menu under the wrong end of the bar once the
capsule moved.

* Keep indented rules literal and renumber wide markers

A four-space indent already keeps a heading and a quote literal, but the
rule check ran before that guard, so `    ---` drew a horizontal rule and
hid its own text.

An ordered marker's stored number drops leading zeros, so `007.` was read
as one digit. Continuing that list took the second zero for its delimiter
and renumbering rewrote only the first digit. Both now take the digit run
from the source.

Also corrects two doc lines the formatting bar's move left behind.

* Drop Carbon from the Notes editor's chords

NoteTextView only needed HIToolbox for the digit key codes that keep the
list and heading chords working on a non-US layout. Those are seven
constants, so it states them itself.

Carbon stays where it earns its place: the global hotkey registration and
the TIS input-source APIs.
2026-09-18 17:38:56 +06:00
Matt Farrellandabue-ammar 91d8b9c20b Define Words (#848)
* Add native dictionary define fallback

* Make dictionary lookup a core launcher command

* feat(dictionary): add dictionary functionality with lookup and display

- Introduced DictionaryEntry model to parse and store dictionary entries.
- Implemented DictionaryProvider to fetch definitions from Dictionary Services.
- Created DictionaryCoordinator to manage dictionary-related actions.
- Developed DictionaryScreen to display definitions in the palette.
- Added fallback command for "define" to trigger dictionary lookups.
- Updated UI components to integrate dictionary features, including copy and open actions.
- Enhanced documentation to cover new dictionary functionality and usage.

* feat(dictionary): enhance dictionary command and fallback functionality

* Render Define Word entries as a structured dictionary page

Read the record's XHTML through Dictionary Services' record calls,
resolved with dlsym so a macOS without them falls back to the public
plain text. DictionaryMarkup turns the span classes into headword,
part of speech, numbered senses, notes and sections, and
DictionarySession looks terms up off the main actor.

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-18 10:13:31 +06:00
Clément Knodererandabue-ammar e25bd13bc0 Add search to action menus (#864)
* Add search to action menus

* Fix action menu keyboard handling

* Refactor symbol name resolution to simplify handling of dark mode and improve code clarity

* Close the palette when a menu action opens a window

An action ran before its menu closed, so a window it opened took key while
the menu's callbacks were still live and `menuOpen` was still mirrored true
a cycle later. Both dismissal guards then bailed and the palette stayed on
screen behind Settings, About and Support.

Close the menu first, and state `menuOpen` in `open`/`closeMenus` rather
than mirroring it from `onChange`, so the window delegate reads it within
the same turn.

Also tidies the header menu symbol initializer's formatting.

* Fix action menu presentation

* Refactor menu height calculations for improved clarity and consistency

* Refactor menu animation durations for improved responsiveness

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-18 07:38:54 +06:00
Emanuel Quimper 92a5d15bce Add interactive Markdown task lists to Notes (#823)
* feat: Add interactive Markdown checkboxes to notes

* fix: Add spacing between note tasks

* fix: Update note tasks incrementally
2026-09-17 11:19:22 +06:00
Abue Ammar 4bc84e2ab8 Add custom window sizes (#819)
* Add custom window sizes (#734)

User-defined window commands: a name, a width and height in points or
percent, and a 3x3 position, applied to the focused window on its own
display. Listed with the window commands, bindable to a global shortcut,
undone by Restore, and carried in settings backups.

* Address review on custom window sizes

Fold imported names without a locale, matching the store's own duplicate
check. Reach the coordinator through the environment instead of AppCore,
and let it decide between add and update. Drop formatting-only edits to
unrelated files that slipped into the first commit.

* Drop unrelated formatting edits from the custom sizes branch
2026-09-17 03:28:15 +06:00
Abue Ammar f51f1c5786 Keep SemVer prerelease tags in installed CLI versions (#811)
`opencode --version` prints `opencode2 v0.0.0-beta-19271`, and the row
showed `Version 0.0.0`. The version match now includes the SemVer
prerelease and build suffixes.
2026-09-17 01:56:05 +06:00
Abue Ammar d4e788fb58 Search and run Apple Shortcuts from the launcher (#756)
Shortcuts from the Shortcuts app become their own launcher section,
read through Apple's /usr/bin/shortcuts tool on every launcher open
and run headless with `shortcuts run <uuid>`. Each row carries an
alias, a global hotkey and a hide checkbox, like any launcher item.

The feature ships off behind one switch in Settings > Apple Shortcuts.
AppleShortcutCoordinator.run(id:) is the single funnel for rows and
hotkeys. A successful, non-empty read sweeps the hotkey, alias,
visibility, favorite and ranking of any shortcut no longer listed; a
failed or empty read frees nothing.

LauncherItemsSection's table half is now LauncherItemsList, shared
with the new pane, and ToolRunner accepts a nil timeout.
2026-09-16 01:22:04 +06:00
Clément Knodererandabue-ammar 423fcfac8f Enhance and polish the Emoji and Symbols picker (#733)
* Polish the emoji and symbol picker

* Split the emoji observers out of the palette's state chain

* Give each header menu its own width and simplify emoji pins and zoom

- Replace fitted menu widths with a stated width per header menu
- Route emoji zoom chords through the panel's command shortcut path
- Count pin positions over the pins the catalog can show
- Move PinnedEmojiStore into its own file
- Restore SwiftUI menu symbols and leave the extension chevron untouched

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-15 22:44:58 +06:00
Quentin Eude dd281ff975 Run extension commands from raycast:// and tinycast:// deeplinks (#674) 2026-09-14 20:51:14 +06:00
Abue Ammar b7822817a9 Let palette screens answer their own shortcuts (#678)
* Let palette screens answer their own shortcuts

RootPaletteView had grown back to 1,513 lines, and much of that was feature code: ten key handlers
that cast `screen` to a concrete type, the AI model menus, and views other files use.

- `PaletteShortcut` recognises each row chord (⌘⌫, ⌃X, ⇧⌘C, ⌘Y, ⇧⌘F, ⌘R, …) and carries its
  compact-bar and open-menu guards; a screen acts on it through `PaletteScreen.perform(_:at:)`.
  One handler replaces the ten, and ⌘. and ⌘1…⌘0 go through the same call.
- The AI model and reasoning menus move to `AIModelMenu`, next to the rest of AI.
- `ArmedHover`, `EmptyResults`, `PaletteBackground` and `CompactFavoritesRow` get their own files.
- The rule for what saving an AI connection does to its Keychain key moves out of the view into
  `AIConnectionKeyPolicy`, and the connection editor sheet gets its own file.

No behaviour change. RootPaletteView is 1,204 lines; AISettingsView is 640.

* Refactor AppSettings to improve readability of palettePositions assignment
2026-09-14 18:07:41 +06:00
Abue Ammar 47fbedd546 Show test progress and timings, and stop harnesses from hanging the suite (#644)
run-tests.sh now numbers each result, prints each harness's run and compile
time, names what is still running during a quiet stretch, kills a harness
that passes TINYCAST_TEST_TIMEOUT (default 300s), and ends on a clear
PASSED or FAILED line.

custom-command-test hung forever in a real terminal: an interactive zsh found
the inherited controlling terminal and was stopped by SIGTTOU. The harness now
calls setsid(), matching the app, which has no terminal.

fuzz-test runs its 100k-query property loop across cores over queries drawn in
one seeded sequence, cutting it from ~25s to ~1.4s.

clipboard-worker-test is removed: it compiled fixtures with xcrun at run time
and hung in 9 of 40 runs, stuck in Process.waitUntilExit after the helper had
exited.
2026-09-14 01:49:48 +06:00
Erik van Eykelen 79144fc5aa Improve emoji keyword matching and search ranking (#627) 2026-09-13 22:46:54 +06:00
Abue Ammar 9102d32da5 Play media the moment Quick Look opens it, and stop one from stretching the panel (#622)
* Play media the moment Quick Look opens it, and stop one from stretching the panel

File Search's ⌘Y overlay now starts a movie or a track as it appears. `FileSearchSurface`
carries one `autoplays` parameter; the preview pane leaves it off, because arrow-keying a
list must not start a movie, while opening Quick Look on one is the ask itself.

The clipboard's media preview asked for a fixed 260 pt whatever the pane had. With the
Information block's 175 pt under it the column wanted 435 pt of a 359 pt content area, and
the overflow pushed the bottom bar out and the panel taller than it is anywhere else.
`clipboardMediaHeight` is now the cap it should always have been, so the player shrinks the
way the image and text previews beside it already do.

* Add keyboard focus handling and related tests for media playback

* Refactor drag handling for empty text fields in the palette search field
2026-09-13 01:38:47 +06:00
Abue Ammar 7caa286ed0 Turn on the hardened runtime, and teach the updater the identity it will switch to (#620)
* Turn on the hardened runtime, and teach the updater the identity it will switch to

Notarization needs two things Tinycast doesn't have: the hardened runtime, and
an Apple Developer ID signature. This does the first and prepares for the second.

The updater compares signatures before it installs, byte-for-byte against the
leaf the running app carries. A Developer ID leaf is a different certificate, so
switching identities outright would make every installed copy reject every future
update. `BundleSignature` therefore learns the Developer ID requirement now, while
releases are still signed with `Tinycast Self-Signed` — the code that trusts the
new identity has to reach people before the first build carrying it does.

The requirement pins the team, not the certificate, so a renewal strands nobody.
It omits the `notarized` keyword on purpose: that resolves a ticket through
syspolicyd or the network, and the updater verifies inside a cache directory
Gatekeeper has never assessed, so an offline Mac would refuse a bundle the chain
already proves is ours.

Hardened runtime needs two entitlements. JavaScriptCore compiles every extension
command, and without `allow-jit` it falls back to the interpreter. Without
`automation.apple-events` every Apple event is refused with -1743 and no prompt,
which silently kills Get Info, the Finder selection extensions read, and the
System Events-driven system actions. Nothing else is required: the only dlopen is
Apple's own IOBluetooth, so library validation stays on.

The flag is not part of the designated requirement, so no Accessibility grant is
lost here. `project.yml` keeps signing with `Tinycast Self-Signed`, so nothing
changes for a contributor building locally.

`Scripts/verify-signature.sh` asserts what notarization will check — the runtime
flag on the app and on the embedded helper, an intact nested seal, and no
get-task-allow. Both release jobs run it before packaging, because a nested binary
missing the runtime flag is the most common notarization rejection there is.

* Keep the hardened runtime out of Debug, where library validation refuses the debug dylib

Hardened runtime turns on library validation, and a Debug build links
`Tinycast Dev.debug.dylib`. The self-signed identity carries no Team ID, so the
loader sees a team mismatch and aborts at launch — every local Debug build died
with a DYLD "Library missing" termination.

Notarization only ever sees Release, so the flag belongs in that config alone.
2026-09-12 22:04:09 +06:00
Abue Ammar 5d8dbb77ab Give Search Files a preview pane, a type filter and an Actions menu worth opening (#612)
* feat: Enhance file search functionality with type filtering and Quick Look support

- Introduced FileSearchFilter to allow users to filter search results by type (All Types, Folders, Documents, Images, Audio, Video, Archives).
- Updated FileSearchScreen to display Recently Used files when the search query is empty.
- Implemented Quick Look functionality within the file search panel, allowing users to preview files without leaving the search interface.
- Enhanced the UI to include a preview pane alongside search results, displaying relevant file information.
- Added keyboard shortcuts for file actions (copy, paste, trash) and Quick Look toggle.
- Improved the handling of empty states and error messages during file searches.
- Updated documentation to reflect new features and usage instructions.

* Read one attribute per Spotlight result, not four

Opening Search Files sat for half a second before its Recently Used rows
appeared, and a broad query took the best part of a second. Neither was
Spotlight: `MDQueryExecute` returns in 15–75 ms. It was `MDItemCopyAttribute`,
which costs about half a millisecond per attribute per result — reading the
content type, the invisible flag and both date stamps over the ~430 candidates
a recents query matches was 430 ms of the 550.

`kMDItemPath` is the exception: `MDQuery` hands it back from its own cache, so
a thousand of them read in 2.8 ms. Everything else a row needs — is it a
folder, is it hidden, is it an application — now comes from one `resourceValues`
stat, taken only for the candidates the ignore list did not already drop. Two
hundred URLs stat in 13 ms where two hundred metadata fetches cost 200 ms.

Recents no longer date every candidate either. Spotlight sorts on one
attribute, so the service runs one sorted query per stamp and merges their
heads; only the first twenty rows of each list can reach the merged one, and
only those are dated. The sort attribute has to be named in `MDQueryCreate` —
set afterwards through `MDQuerySetSortOrder` it is ignored, which is why the
first version had to sort locally. The blank screen also skips the typing
debounce, having no next keystroke to coalesce with.

Measured on the developer home, release-optimized: recents 41 ms on a repeat
against ~550 ms, and the five benchmark queries 54–107 ms against 192–831 ms.

Building the expressions moved with the queries into the service, where the
policy that shapes them already is; the session now owns only when a search
runs. Move to Trash takes ⌃X, the chord the clipboard's delete already uses,
and Paste File moves up beside Copy File in the Actions menu.

* Let the preview play, in the pane as well as the overlay

The overlay's dismissing tap gesture was laid over the whole card, preview
included, so the click that should have hit a movie's play button closed the
overlay instead — the transport drew, took the press and never saw it. Only
the margin around the card dismisses now; anything over the preview belongs to
the preview.

The pane beside the list shows the file itself for the same reason it was
worth having Quick Look at all: a still says nothing a movie or a long document
needs said. `QuickLookSurface` moves out of the overlay into its own file and
mounts in both, and the pane's copy waits 180 ms for the selection to hold, so
arrow-keying a list of two hundred rows opens no preview it is about to drop.
The thumbnail stands in until then.

The live view is torn down whenever the palette is ordered out, the overlay
covers it, or a folder is selected — one key over all three, so no `onChange`
races the task, and never two preview extensions running for one file.

* Give the preview its own player, 16:9, and an Escape AVKit cannot eat

Three things the preview pane still got wrong.

A movie never played. `QLPreviewView` draws a first frame and hands out a
transport, but inside a non-activating panel it does not play, so movies and
audio now go to an `AVPlayerView` of File Search's own. It is a copy of the
shape the clipboard's preview uses rather than a share of it: that pane is a
different surface with its own sizing and its own lifetime, and forty lines of
teardown is the cheaper trade against coupling the two.

The stage is 16:9 and sized before the block beneath it. Without the layout
priority the aspect ratio fits itself into whatever height the Information rows
left over, which is backwards — it shrank the preview to 328×173 instead of
filling the pane's width at 436×234. The rows now scroll in what is left,
through the plain scroll view the clipboard's preview uses, since a thin
scrollbar over a document is chrome on chrome.

Escape did not close Quick Look once a player was in it: a focused
`AVPlayerView` answers the key window's Escape before SwiftUI's handler ever
sees it. `PalettePanel.sendEvent` is the one place ahead of the responder
chain, so the panel now owns that press while the overlay is up, and
`PaletteEscapeAction` goes back to the shape it had. The Close chip is a button
now too, for the pointer.

* feat: Add FileSearchFileView for unified file preview handling in search results

* feat: Replace FileSearchFileView with FileSearchSurface for unified file handling in search results

* Hand the preview another file rather than building it a new one

Clicking a row went blank before it went live. The stage gated the whole
surface on a flag that flipped false on every selection, so each move
dismantled the `QLPreviewView`, closed it, showed `Color.clear`, then built a
fresh one — a teardown and a rebuild to show the next file.

The surface now outlives the selection: only the settled URL changes, so a
move hands the same view another item. The settle drops to 80 ms, which still
coalesces a held arrow key and no longer reads as a wait on a click. Measured
against the real QuickLook machinery: the first load in a process is ~130 ms
and every load after it ~10 ms, so what a click waits for is the settle, not
the preview.

Text goes back to QuickLook with it. Drawing it here fixed the wrong thing —
its scroll view is configured exactly like the palette's own, overlay style and
autohiding, and QuickLook renders a document better than a monospaced `Text`
in a `ScrollView` does.

* Say in the feature doc what the preview surface now does

* Answer a click on the press, and drop the timer in front of the preview

The second a click took was SwiftUI waiting: `.onTapGesture(count: 2)` cannot
deliver the single tap until the system's double-click interval has passed
without a second press, so the selection — and the preview that follows it —
sat still for that whole window. The clipboard's rows never felt this because
they select in `mouseDown`. File Search now does the same, through an
`onRowClick` catcher beside the right-click one already in `DesignSystem`:
select on the press, open when the press is the second.

The settle goes with it, and the state it needed. It was guarding a cost that
is not there: measured against the real QuickLook machinery inside a panel
shaped like the palette's — borderless, floating, non-activating, never key —
handing a live `QLPreviewView` another file paints in about 8 ms, and the
first load in a process in about 130 ms. Debouncing 8 ms of work bought
nothing and spent 80.

What is left is a surface mounted while it should be on screen and unmounted
when it should not, which is also the whole of its teardown: no duration, no
flag, no task.
2026-09-12 18:35:01 +06:00
Abue Ammar 2a93f66ed8 Reach a window or a menu bar item from the launcher (#602) (#603)
Adds a Navigation feature: one settings pane, one switch, two commands
that move you somewhere rather than changing something.

Switch Windows sweeps every regular app's standard windows over AX,
minimized ones included, orders them most-recently-used and raises the
chosen one. Recency comes from a single CGWindowListCopyWindowInfo call
for per-app front rank — public API, no Screen Recording grant and no
long-lived activation observer.

Search Menu Bar Items is today's Search Menu Items, moved out of
Settings > Commands into the new pane and renamed. Its raw id is
unchanged, so recorded shortcuts, aliases and visibility keys survive.
It also gains a Disabled Applications list: MenuSearchTarget.classify
answers .excluded before the menu-bar test, so an excluded app starts no
walk at all rather than having a read menu filtered afterwards.

The exclusion list is the Clipboard section lifted into a shared
DisabledApplicationsSection, and AppPickerPopover moves to
Features/Launcher/Settings/ where its three consumers already pointed.
2026-09-12 04:50:08 +06:00
electro56435andabue-ammar e2e3bf6113 Drag a clipboard entry out into another app (#596)
* Spell out the Array conversion in the OCR search tail

Left open, the type checker reads .prefix as the Sequence overload, types
the result as PrefixSequence and stops resolving the whole + chain. The
Swift 6.2.1 toolchain in the Command Line Tools rejects the line outright,
which takes clipboard-test, clipboard-search-test and every other harness
that compiles the store down with it. Behaviour is unchanged.

* Drag an image or file out of clipboard history

An image or file row becomes a drag source for the file it already is, so
reaching another app costs one gesture instead of Reveal in Finder plus a
second drag.

The drag is AppKit rather than SwiftUI's onDrag for one reason: imagesDir
sits on the boot volume, where a file-URL drop defaults to a move, and a
move carries the blob out of the history and strands its row. Only an
NSDraggingSource can answer sourceOperationMaskFor, so ClipDragView
answers .copy everywhere. The handle claims mouse-down the way
WindowDragHandle does, since the hosting view eats the click first, and
forwards anything under 4pt of slop as the click it was.

Text rows are untouched: dragURL is nil for them, so no overlay is
installed and their gestures stay as they were.

* Drag a link and plain text out too, not only a file

A text row had no payload and so no overlay, which left every entry that
is not an image or a file undraggable. dragPayload now answers for all
three: the file URL for anything on disk, and for a text row the
classification the store already derives.

A link writes two pasteboard types from one item. A browser reads
public.url, a text field reads the string, and neither has to settle for
the other's flavour. A bare domain gets https:// so the URL is one a
browser accepts, which is the same assumption the address bar makes.

The drag preview is now the row as drawn, since a text row has no
thumbnail to fall back on.

* Fix the drag preview, and move the payload off the store

Four things the review caught.

The row snapshot never drew anything. SwiftUI renders into layers, so
cacheDisplay hands back a transparent bitmap and the drag carried no
image at all. Previews are drawn per payload now: the cached tile for a
file, a rounded text tile for a link or a copy. The dragging frame is
sized to that image and centred on the cursor, and a refused drop
animates back, so a drag that achieved nothing says so.

dragPayload touched no store state and pushed ClipboardStore past 1000
lines. It is a computed property on ClipboardItem now, in its own Model
file beside the ClipDragPayload it returns, which is where textForm and
colorValue already live.

The bespoke bare-domain helper is gone. QuicklinkDestination.detect
already parses schemes, network shares and deeplinks. textForm stays the
one answer to whether an entry is a link, so the drag and the type filter
cannot disagree; the detector only builds the URL.

A vanished file is reported rather than dragged out as a dead path, which
is what Reveal and Open already do. The payload resolves on mouse-down
instead of on every row render, so the stat costs one call per drag.

Also deletes the row's onTapGesture and double-tap gesture. The overlay
claims mouse-down on every row, so both were unreachable duplicates of
the closures the handle already calls.

* Rewrite the drag docs against the code that shipped

The section still described dragURL on the store, a nil payload for text
and the onTapGesture the overlay replaced, all three of which went in the
last two commits. It now covers what is there: dragPayload on the item,
the mouse-down resolution and its stat, the QuicklinkDestination reuse,
and why previews are drawn rather than snapshotted.

* Let a right click through, and drop the deprecated lockFocus

Three things from the review.

The drag overlay sat above the actions catcher and answered every event
it was offered, right-mouse included. NSView forwards an unhandled
rightMouseDown up the superview chain, never to a sibling, so the catcher
underneath was unreachable and the row's actions menu silently stopped
opening. ClipDragView now declines right events in hitTest, the mirror of
what RightClickCatcher already does with the left button, so neither
overlay can claim what the other needs. Verified against a real event
loop: the right click lands on the catcher and the left one still starts
the drag.

lockFocus and unlockFocus are deprecated, and the SDK names
NSImage(size:flipped:drawingHandler:) as the replacement. The text tile
draws through that instead.

The comments were stacked two and three lines deep. Each is one line now,
and the ordering invariant the overlay depends on is written down in the
feature doc rather than argued in the file.

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-12 02:48:14 +06:00
Abue Ammar 9ad7f376ce Scale the palette with an Interface Size setting (#597)
Tinycast rendered every surface at one fixed size, which reads small on a
large display and fine on a laptop. General ▸ Appearance now carries an
Interface Size control — Default, Large, Larger — that uniformly zooms the
palette and the surfaces that float with it, at 1.0 / 1.1 / 1.2.

The setting reaches the palette, the ⌘K menu, the extension list panel, Quick
Actions, the snippet prompt, dialogs and HUDs. Settings, Onboarding, Support,
Update, About and Notes never scale: a zoom there only breaks their layout.

`InterfaceMetrics` stores a scale and nothing else, deriving every value from
the `Theme` literal, so `Theme` stays the one place a number is written down.
It reaches views through an `@Entry` environment key defaulting to `.standard`,
which is why `BarButton`, `KeyCapChip`, `PopoverMenu` and the rest render
unscaled in Settings without being forked — the scope is the injection, not the
component. An AppKit site reads `settings.interfaceSize.metrics` where it
computes its frame, so no second owner of the value exists.

Two things in this are not obvious from the diff.

A scaled font is rebuilt from that style's own `NSFontDescriptor` at the scaled
point size, never reconstructed as `.system(size:weight:)` from a written-out
weight table. On macOS `Font.headline` resolves to `.SFNS-Bold` and
`Font.caption2` to `.SFNS-Medium`, so a table lightens both the moment the user
leaves the default size. The same helper yields the `NSFont` twins the caret
width and the AI chips are measured against, which have to move in lock-step
with what SwiftUI renders or the caret detaches from the text.

And the palette's environment is pushed by a `ViewModifier` rather than a
stored `.environment(_:_:)` value. `PaletteWindowController` builds the hosted
tree once and reuses the panel, so a stored value would have frozen at
build time and the setting would never have taken effect — not even on the next
summon.

A length measured against the screen does not scale; a length measured against
our own content does. So `hairline`, `paletteTopMarginFraction`,
`paletteSnapDistance`, `paletteMinimumVisible`, the drop-guide dashes,
`hudEdgeOffset` and every row *count* stay on `Theme`. Scaling rounds to whole
points once, at the leaf accessor, and a derived token composes already-scaled
parts rather than scaling the derived result, so an AppKit frame can never
disagree with the SwiftUI view inside it by a point.

A size change re-enters through `AppCore.track` → `applyInterfaceSize()`, which
drops the cached anchor and re-resolves it — one rule, the summon's. An
untouched palette re-centres at the new width; a dragged one keeps its stored
top-left unless the wider bar no longer leaves `paletteMinimumVisible` on any
display, in which case it falls home. `PalettePanel`'s stale `750, 475` literal
is gone; it reads the tokens.

`ExtensionFormMetrics` becomes a struct taking a scale, staying inside
`Features/Extensions/` and Foundation-only. `EdgeDissolve` derives its bands
from the metrics and resolves to the same 86 and 80 it draws today.
2026-09-12 02:22:01 +06:00
Abue Ammar 69aacc188b Expand snippets inside a Note (#592)
Snippet keywords did nothing in a Note, and the reason was the destination
rather than the delivery: `TextInjector` resolved its target from
`NSWorkspace.frontmostApplication`, but every Tinycast panel is
non-activating, so with the Note key the frontmost app is still the one
behind it. The expansion was being posted as events at that app.

The destination is now an `InjectionTarget`, resolved from `NSApp.keyWindow`
first, and one of our own editors is written in process — undoable in the
editor's own UndoManager, with no Accessibility grant, pasteboard lease, app
activation or event posting. A key window of ours that is not an
`InjectableTextView` resolves to no target at all, so a keyword typed in the
palette's search field no longer expands into whatever sits behind it either.

The tap is a `headInsertEventTap`, so it fires before AppKit hands the
keystroke to our own view: the first look at the text storage is always one
character stale, and `.pending` only covers a document shorter than the
keyword. This tier therefore leads with one convergence interval before it
inspects, then polls the shared budget — in practice the keyword has landed
after a single 5 ms pass.
2026-09-12 00:42:48 +06:00
Abue Ammarandjoaogsleite 992c3dcdfc Search Menu Items for the frontmost app (#587)
* Add Search Menu Items for the frontmost app

Fuzzy-searches the frontmost application's menu bar from the palette
and activates the chosen item, via the Search Menu Items launcher
command or its bindable (unbound by default) global hotkey.

The bar is walked once per show, off-main, into plain MenuTreeNode
values: 20 levels deep, 4,000 items, 200 direct leaves per submenu,
1-second budget, every cap truncating silently. Attribute reads are
batched per leaf and only enabled, visible, pressable leaves become
rows, each with its full menu path and shortcut glyph (Shift/Option/
Control bits decoded live, unrenderable keys degrade to path alone,
private-use scalars mapped to arrow and page glyphs). Filtering
reuses the launcher's fuzzy tiers over the frozen snapshot, ranked
once per query change and capped at 200 rows. Activation re-resolves
the row by path, re-checks it, and presses via AXPress after
dismissing the palette and reactivating the frozen target; failures
report through the dialog controller.

Accessibility gates show and activate with an Open Settings recovery.
No new AppEntry.Kind, no visibility gate, no frecency learning, and
hiding the palette resets the session.

Invariants and internals are documented in docs/features/menu-search.md.

Originally submitted as #586. Rebased onto main, formatted, and
documented in a feature doc while folding in a small dedupe of the
shortcut glyph ordering.

Co-authored-by: abue-ammar <iabueammar@gmail.com>

* Drop derivative framing from comments and docs

Removes the wording that described Tinycast's own design as derived
from another launcher: "a port of", "-style", "as Raycast does",
"exactly as in Raycast", "Raycast's own shape/form". Tinycast's
surfaces are its own, and the comments now say what they do and why
rather than what they were measured against.

Every reference where Raycast names something Tinycast actually
reads, parses, fetches or must stay API-compatible with is left
exactly as it was: the extension API and runtime, raycast:// URLs,
raycast-* colour keywords, the .rayconfig importer and RAYCFG3
container, script-command headers, store endpoints, on-disk install
layout and accepted token spellings. Those name a real dependency,
and deleting them would delete the reason that code exists. The
measured comparisons in docs/features/uninstall.md stay too; they
state where Tinycast deliberately differs.

Comments and prose only; no behaviour changes. Two stacked-comment
and three line-length violations are fixed in passing.

---------

Co-authored-by: joaogsleite <joaogsleite@gmail.com>
2026-09-11 22:07:53 +06:00
Abue Ammar 4cc3385632 Cycle halves across displays (#563)
* Cycle halves across displays

Adds a Cycling picker to Window Management, matching Raycast's own
setting: None, Cycle ½ ⅓ and ⅔, or Cycle displays.

Under Cycle displays every display contributes two half-slots to one
strip, ordered left-to-right. Left and Top walk it backwards, Right and
Bottom forwards, both wrapping, so one shortcut sweeps the whole desktop
in one direction. A single display makes the mode a quiet no-op, the
same call Next Display already makes.

A private Half type in the engine carries the (axis, edge) pair, so a
slot's edge decides which side a third hugs and the four halves stop
being four hand-written fraction cases. WindowActionMemory now takes a
cycleLength rather than a Bool: a length of 1 covers both cycling off
and a command that never cycles, so it no longer reads the catalog.

Closes #552

* Default the cycling picker to None, not the size cycle

`windowManagementCycleOnRepeat` defaulted to false, so a repeat press
re-applied the same half on every install that never opened the setting.
Renaming the key to `windowManagementCycleMode` means nobody holds it on
first launch after the update, and a `.sizes` fallback would have turned
size cycling on for that whole population — a visible behaviour change
for users who never asked for one.

`.off` keeps the shipped default. Choosing a mode stays a deliberate act,
which is also what makes `Cycle displays` discoverable rather than
something a window starts doing on its own.

Brings the website docs along: the Settings table row, the reference
table and the Cycling section all still described the old checkbox.
2026-09-11 05:54:25 +06:00
Abue Ammarandoverflowy f80db5187f Custom quick actions (#555)
* Split QuickAction into the shipped four and the user's own

QuickAction becomes a sum over BuiltInQuickAction and a CustomQuickAction
record, so every surface downstream keeps taking one type. Custom actions
are name, SF Symbol and instructions, stored in their own file-backed
CustomQuickActionStore under Application Support, and never in a backup,
like the instruction overrides they resemble.

The untrusted-input boundary is prepended to a custom prompt and cannot be
edited away: an override on a built-in replaces it by design, but a whole
prompt written by the reader has nothing else standing between the model
and the text it is handed.

* Give Quick Actions their own launcher kind, section and shortcut

AppEntry.Kind.quickAction is the new section; the four built-ins move out
of Commands into it, keeping their CommandIDs so no shortcut or preference
key changes. A custom action reaches the launcher through the same kind and
binds a chord through HotKeyAction.quickAction(id:).

allowsHotKey now gates a command on the kind its entry actually reports, so
both halves of the feature answer to one switch rather than two.

* Own the custom action store from AppCore, and run its actions

The store loads before hotKeys.start so a binding whose action was deleted
while Tinycast wasn't running is pruned. Deleting confirms through
DialogController and unwinds the shortcut, favorite, alias, visibility and
ranking the row held, only once the record itself is gone.

* Add, edit and remove custom actions in the Quick Actions pane

Custom rows sit below the shipped four in one Actions list, each with the
same pencil, recorder, Replace/Preview picker and launcher checkbox. A plus
in the section header opens a blank editor.

* Register the new sources and cover custom actions in the harnesses

* Update the Quick Actions and launcher docs for custom actions

The quick-actions doc said there were four actions and that a fifth was one
case in QuickAction. Both stopped being true. It now covers the sum type,
the record and its store, why Replace/Preview lives on the record, why a
custom prompt cannot drop the untrusted-input boundary, why the pane draws
its own alias field, and why nothing is saved until it is on disk.

The launcher doc gains the shared Quick Actions kind, the quick-action entry
ids, the new hotkey action, why a Quick Action command goes through
allowsHotKey ungated, and the note that Enable Commands no longer stops the
shipped four.

* Widen the Settings window for the Quick Actions alias field

Every Quick Actions row now draws an AliasField, which put the widest row's
trailing controls at roughly 430pt and left about 35pt of slack at 860. 900
gives back the room a long custom action name needs.

---------

Co-authored-by: overflowy <overflowy@riseup.net>
2026-09-11 03:29:49 +06:00
Jan Kamplingandabue-ammar ddbf87b3f5 Add opt-in OCR search for clipboard images and PDFs (#522)
* Add opt-in OCR search for clipboard images and PDFs

* Fix clipboard OCR scheduling, retries and retained memory

* Refactor async extraction calls and improve error handling in clipboard tests

* Tighten clipboard OCR wiring, release guards and docs

The helper's read loop and `waitUntilExit` both block, and they ran inline
on the cooperative pool, so a stuck helper could hold a pool thread for the
whole 60-second deadline. They now run on their own `DispatchQueue`, which
is what `ShellCommandRunner` already does and says why.

`AppCore` no longer keeps a transition task to wait for the indexer's
teardown before starting another. The indexer's own `defer` already
reschedules after a cancel-while-enabled, and `start`, `stop` and that
`defer` are all MainActor-isolated with no suspension between them, so the
wait bought nothing. One indexer now lives for the app's life, and
`clipboard-text-test` covers the un-waited stop-then-start that relies on
it — breaking the reschedule fails the harness.

Both release jobs asserted architecture slices on `Contents/MacOS` alone,
which a bundled helper now sits beside. A thin helper inside a universal
build is the quiet form of the bug that guard exists to prevent: the app
still boots on Intel, `posix_spawn` fails with `EBADARCH`, the indexer
burns three retries per item into private OSLog, and clipboard OCR simply
never answers. Both jobs check both binaries.

In the store, `insertExtractedText` hand-rolled a prepare/finalize the
store already has a helper for, so it folds into `setExtractedText`. The
200-row result cap appeared in four places and is now named, and an
`OFFSET 999` that silently duplicated `memoryWindow` derives from it.
`extractionGeneration` and `textSearchEnabled` are `@ObservationIgnored` —
no view reads either — which leaves `searchRevision` as the single observed
search signal, and it now says why it is read for its own sake.

`reinsert` lost its doc comment and the note about why the array ops are
separate while it was being rewritten; both are back, and the new one
records that a delete there would take the row's recognized text with it.

`docs/performance/` is deleted: nothing referenced it, `main` has no such
tree, and per-run measurement dumps belong in a PR rather than the repo.
The feature and testing docs lose the PR-report prose that came with them,
and the claim that re-enabling waits for cancellation, which is no longer
how it works.

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-11 02:01:49 +06:00
Anthony e3019fffe5 Render an extension's search-bar dropdown, and report its opening choice (#515)
`List.Dropdown` and `Grid.Dropdown` reached `ExtensionScreen.searchBarAccessory`
and were read by nobody, so a command that filters its rows from the dropdown
drew an empty list — the Visual Studio Code extension's Search Recent Projects
holds `useState<EntryType | null>(null)` and its predicate rejects everything
until `onChange` fires. Raycast reports the opening choice itself, so two
things were missing, not one: the control, and the first dispatch.

The dropdown becomes an `OpenMenu` case, the same shape the clipboard type
filter and the two AI menus already have. Arrows, ↵, Escape, the click-away
catcher and the ⌘K suppression all come from the one menu path, so no second
key handler exists to disagree with it, and `ExtensionShortcutKeys` is gated on
`menuOpen` for free — an extension's own single-key shortcuts can no longer
fire while its filter list is up.

Swift owns the selection, because the runtime keeps `makeSearchDropdown`
hook-free so an extension may call `List.Dropdown({…})` directly:
`accessoryValues` keys the pick by render-node id, so a pushed screen and the
one under it keep their own. A `value` prop makes the dropdown controlled —
the extension holds it, nothing is seeded and nothing is reported back — which
is what removes any need to remember what was last dispatched.

`storeValue` parks the pick in `ExtensionStorage.accessoryValues`: host UI
state, outside the `LocalStorage` namespace JavaScript reads, and dropped with
the extension on uninstall. `Store` now decodes section by section, because
adding a non-optional field to a file read with `try?` would have reset every
installed extension's store on first launch, losing its `LocalStorage` and its
saved preferences alike.

Everything extension-shaped stays in `Features/Extensions/`, including the
list's own 240pt width — a form picker's 360 belongs to the field it sits
under, not to a header chip. The palette learns only that the running command
may offer a header filter; `PaletteFilterAction` carries the ⌘P decision as a
pure type beside `PaletteEscapeAction` and `PaletteTabAction`, so the one hunk
that touches an existing path is covered by a harness rather than argued.

`extensionCommandScreen` gates on the mode before the cast. Reaching it costs
the running mode's own list build, and the header reads it on every render, so
an unguarded cast would have charged the launcher a second `LauncherScreen` per
keystroke to answer a question only an extension can ever say yes to.

Closes #511
2026-09-10 02:58:11 +06:00
Jan Kamplingandabue-ammar f5502036ae Bound clipboard URL decoding for large file selections (#513)
* Bound clipboard URL decoding by the capture limit

* Simplify the bounded reader to a single pass

The batched reader is replaced by one loop that filters while it decodes, and
`urls(on:)` becomes that reader with no limit, so the rule that a modern file
URL suppresses the legacy fallback is written once rather than twice.

Head to head the two are a wash: batching is ~6% faster once files are accepted
and ~7% slower on a rejected prefix, both ~70x better than the base, and the
difference is 0.05ms on a 0.5s poll.

macOS synthesizes `public.file-url` items for every `NSFilenamesPboardType`
write, so the raw legacy fixture tested a branch the system never reaches; it
and its runtime class swizzling are dropped. The limit boundaries, the
predicate call counts and modern/legacy precedence are kept, and an
uncapped-reader control guards the attachment path against the delegation.

* Give every harness under Tests/ editor flags

Five files in Tests/ had no .compile entry, so every symbol in them was
unresolved in an editor. Two separate gaps caused it.

The four hand-compiled benchmarks never reach run-tests.sh, which is also what
emits editor flags. `run index <name> <source...>` registers one for --index
without queueing it, so they earn flags and the suite still reports 62.

A helper compiled into another harness, ext-list-key-test.swift, was never
claimed either: an entry claimed only its own harness. Claiming everything
under Tests/ fixes it and stays safe, since the app compiles nothing there.

* Refactor JSON construction and improve code formatting in tests

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-09 23:43:15 +06:00
Quentin Eudeandabue-ammar 2364cd1164 Background refresh for no-view extension commands (#495)
* Refresh no-view extension commands in the background on their manifest interval

* Keep command metadata out of the extension data file

Drawing a launcher row read every command's metadata, and that metadata
lived in the same file as the extension's LocalStorage and Cache, so
publishing the launcher entries faulted every installed extension's whole
store in synchronously on the main actor and held it there. Give the
metadata its own small file, outside extension-data so the cleanup sweep
doesn't read it as one extension's own. ExtensionStorage returns to what
it was, which retires the tolerant decoder it had grown for the key that
is no longer there.

The store is Observable, so the manual metadataRevision counter goes with
it. A cancelled tick no longer records itself as a timeout: cancelling the
loop preempts the run the way a manual launch does, and only an elapsed
timeout counts as a failure. The Settings toggle now gates on the parsed
interval rather than the raw string, so an unparseable one gets no switch
it can never honour, and a failure is cut to its headline before it reaches
AppEntry rather than at render.

* Say what the extensions-stay-inside rule actually forbids

"Never shared with another feature" reads as a ban on any other feature
rendering an extension-owned view, which LauncherScreen has done with
ExtensionArgumentsAccessory since extensions landed. The rule is about
where a view is owned, not which file renders it: the danger is an
extension's shape reaching DesignSystem or Theme and forcing a change on a
palette surface, not a launcher row embedding a box it never looks inside.

---------

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-09 02:05:43 +06:00
Jan Kamplingandabue-ammar 1cce6ff750 Reduce Quick Actions diff matrix memory (#503)
The LCS matrix is the largest allocation a Quick Actions diff makes: at the
4,000-token cap it is 4,001 x 4,001 cells, and an Int cell spends eight bytes
holding a number that never exceeds 4,000. Store the lengths in UInt16 instead,
which drops peak process RSS for a capped diff from about 141 MB to 42 MB.

No LCS length can exceed maxTokens, so the narrower cell cannot overflow;
text-diff-test asserts that relationship and the exact chunks at the cap, one
token under it and one token over. Tokenization, recurrence, traceback,
tie-breaking, coalescing and the over-cap fallback are unchanged.

Co-authored-by: abue-ammar <iabueammar@gmail.com>
2026-09-09 01:03:07 +06:00
Abue Ammarandnotsoshu e3b9f43eb8 Make Escape walk back through the screens it opened (#504)
* feat: Implement Escape Key Behavior settings and navigation enhancements

- Added EscapeKeyBehavior enum to define actions for the Escape key in the palette.
- Updated AIChatCoordinator to use palette.replace() for chat navigation.
- Modified SettingsBackup to include escapeKeyBehavior property.
- Enhanced PaletteState to manage back navigation and screen states.
- Updated PaletteCoordinator to handle navigation logic for existing screens.
- Improved RootPaletteView to reflect new Escape key behavior and navigation logic.
- Added tests for palette navigation to ensure correct behavior of back steps and screen states.
- Updated documentation to reflect changes in Escape key functionality and navigation.

Co-authored-by: notsoshu <319949436+notsoshu@users.noreply.github.com>

* feat: Add CommandEscapeTap to handle ⌘⎋ key chord and update tests for navigation and escape behavior

Co-authored-by: notsoshu <319949436+notsoshu@users.noreply.github.com>

* feat: Add hover effect and back navigation button to enhance user experience in the palette

---------

Co-authored-by: notsoshu <319949436+notsoshu@users.noreply.github.com>
2026-09-09 00:45:16 +06:00