* 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.
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.
* 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.
* Give zlib buffered stream constructors, so PNG-writing extensions run
* Replace an extension toast with the next one, so a finished task clears its spinner
`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.
* 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
undici touches four things at module scope that the runtime refused or
lacked, so Apple Mail and MyIP threw before their first render.
Give diagnostics_channel real channels, make worker_threads'
markAsUncloneable family no-ops so undici's `|| fallback` guard is moot,
add crypto.getHashes for the digests the host computes, and polyfill
Event, EventTarget, MessageChannel and MessagePort beside TextEncoder.
tracingChannel and the rest of worker_threads still refuse on use.
JavaScriptCore settles WebAssembly.compile/instantiate from a run-loop
timer on the thread that owns the VM, and the runtime's queue never
spins one, so the promises stayed pending forever. sql.js loads that
way: Zotero's Search Database sat on Loading… with no error.
Route compile, instantiate and the streaming forms through the
synchronous Module and Instance constructors, and cover them in
ext-test with a module that must instantiate both ways and an invalid
one that must reject with CompileError.
Co-authored-by: Richard Demann <richard.demann@gmail.com>
* 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.
* 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>
* Promote feature enable controls in Settings
* Reuse the shared feature toggle label
* Keep Search Scopes available when Applications is off
* Restore search highlight on launcher settings lists
* Stream spawned child output to extensions as it arrives
* Keep a Detail's markdown image on screen while it updates
* Arm spawn timeouts at launch and keep streamed UTF-8 characters whole
* Emit spawn before a child's output and stream it in the runtime fixtures
* 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
* 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>
* 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>
* 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
* 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.
The lazy-stub Proxy behind every resolve-but-refuse module has a `get` trap
and nothing else. esbuild's `__toESM` never gets a property: it snapshots
`Object.getOwnPropertyNames(mod)` and copies those, so with no `ownKeys` trap
the own keys fall through to the literal target and every manufactured member
is dropped. That is what every namespace or named import compiles to, so the
intended "… is not supported in Tinycast extensions" refusal never fires.
What the extension sees instead is `undefined`, and a `class X extends
<undefined>` turns that into `TypeError: The superclass is not a constructor`
at import time, in a stack that is all `__commonJS` frames and names nothing.
Give the Proxy `ownKeys` and `getOwnPropertyDescriptor`, backed by Node's own
function-valued export names per module. Constants stay out: a member made
here is always a throwing callable, which is the wrong value for one. `http`,
`https` and `stream` stay out too — they are partially supported, their real
classes are already own properties, and phantom keys would flip a working
`typeof stream.isReadable === "function"` fallback into a throw.
`async_hooks` was a plain object literal rather than one of these proxies, so
`AsyncResource` was undefined outright and undici's `class … extends
AsyncResource` produced the same error with no message at all. It gets a real
one — a single synchronous context means the scope is just the call.
Closes#852
* 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.
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.
* 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>
* 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.
* 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.
* 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.
* 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>
* 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>
Deploy the Next.js export to Cloudflare as an assets-only Worker and serve it
from the domain root, so `basePath` and `src/lib/asset.ts` both go away.
The old GitHub Pages URL keeps working: `website/redirect/` is now a lone CNAME
file, and a custom domain on a project site is what makes GitHub 301
`abue-ammar.github.io/tinycast/<path>` to `tinycast.dev/<path>` from its own
edge — repo prefix stripped, path, query and fragment carried, no HTML parsed.
Verified against a live project site before relying on it.
Workers caps a single asset at 25 MiB and the 26.5 MiB tour video breaks it, so
media that size moves to `website/media/`, out of the export, and is served from
an R2 bucket behind cdn.tinycast.dev. Its own workflow mirrors the folder so a
docs typo never re-uploads it.
Every docs page inherited the root layout's `canonical: "/"`, which told Google
the homepage was the real version of all 37 of them. Canonicals are per page
now, with the trailing slash the host actually serves, and the sitemap matches.
Drops the unmaintained macOS 15 Sequoia cask from the docs, the README, the
issue templates and the release-notes tag filter.
* 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
Downloads Manager lists its folder with opendirSync and a readSync loop,
so every command threw and showed an empty list. Dir now walks a snapshot
from the host readdir, in sync, callback and promise forms.
`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.
Google Search failed with "Cannot call a class constructor without new":
safer-buffer copies Buffer's statics with for…in, found none on our class
shim, and fell back to calling Buffer bare.
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.
* 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>
fsPath degraded any URL to its decoded pathname, so fs.existsSync accepted
a vscode-remote:// workspace whose stripped pathname exists locally — an
SSH host opened at / always does — and Raycast's Visual Studio Code
extension handed that URI to fileURLToPath, crashing Search Recent
Projects with "The URL must be of scheme file". URL arguments now go
through fileURLToPath: a non-file scheme throws ERR_INVALID_URL_SCHEME
like Node, existsSync counts that as absence, and the remote entries
render as remote items.
Hide My Email failed at load with "The superclass is not a constructor".
It hands axios a cookie jar through axios-cookiejar-support, whose
http-cookie-agent and agent-base extend http.Agent when the bundle loads.
The http shim had no Agent, and esbuild's namespace interop copies only
the Proxy target's own keys, so the superclass was undefined.
http.Agent is now a real class whose addRequest does nothing, because
the bridge owns every socket. ClientRequest calls it only for an
http.Agent subclass, exposes Node's protocol/host/path, and runs
_implicitHeader in end(), which is where the cookie agent sets Cookie.
Any other agent shape is still ignored, as before. url.format now takes
a parts object, which the cookie agent builds for each request.
URLSession folds repeated Set-Cookie headers into one comma-joined line,
so a jar kept only the first cookie and iCloud login could not finish.
IncomingMessage splits it back into Node's array without cutting an
Expires date, and rawHeaders repeats the name per cookie.
`time in uk` earned no card: the zone lookup only knew IANA city names and
a curated alias table, and Foundation carries no country for a zone.
Scripts/gen-countries.js now emits CountryZoneData.generated.swift by
joining IANA's zone.tab, which lists each country's zones most populous
first, with CLDR's English territory names, short forms included. Where
zone.tab's geographic order puts a remote edge first (Lord Howe, Kaliningrad),
the country answers with its capital's clock instead. CalcTimeZone checks
aliases, then cities, then countries, so no existing name changes meaning.
`usa` and `uae` join the aliases, since CLDR carries neither.
Timers pauses a countdown by storing the pid `exec` returns and later calling
`process.kill` on it. The shim had no `process.kill`, and async `exec` always
reported pid 0, so pausing threw a TypeError.
Async `exec`, `execFile` and `spawn` now launch their child synchronously on the
JS queue, so the handle carries the real pid, and a new `proc.wait` host call
collects the output off that queue. `process.kill` and `ChildProcess.kill`
signal the child for real, and refuse any target that would signal Tinycast
itself (0, -1, its own pid or process group). `os.constants.signals` now lists
every Darwin signal.
Two process bugs made worse by launching on the JS queue are fixed too: stdin
is written after launch on a background thread, so input over 64 KB no longer
wedges the runtime (nor SIGPIPEs Tinycast when the child exits early), and env
values are stringified like Node, so `{ FOO: 1 }` no longer drops the override.
Closes#687
Bitwarden derives its session hash with `crypto.pbkdf2` after `bw unlock`, and the shim had no such
function, so the TypeError surfaced as a credentials failure and the vault re-prompted on every
launch. Its vault cache and Easy Dictionary's Caiyun config also need `createCipheriv` and
`createDecipheriv`.
The shim now provides `pbkdf2`, `pbkdf2Sync`, `createCipheriv` and `createDecipheriv` for AES-128,
-192 and -256 in CBC or ECB, with `setAutoPadding`, backed by CommonCrypto. A cipher buffers its
updates and runs one host call in `final`. Decryption strips PKCS#7 padding itself, because
CommonCrypto accepts padding OpenSSL rejects and a wrong key would otherwise return garbage instead
of `ERR_OSSL_BAD_DECRYPT`.
Closes#643
* 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
* Fix Node CPU metrics for system monitor extensions
* Complete system monitor OS metrics and helper support
* Forward extension list selection changes
* Extract extension selection forwarding
* Seed extension selection from selectedItemId
---------
Co-authored-by: Jonas List <Jonas.List.1289@gmail.com>