Commit Graph
286 Commits
Author SHA1 Message Date
Kaede Akino bf8be6647b Rename network speed labels and refresh product screenshots 2026-09-11 00:19:27 +08:00
Kaede Akino afa77ae16a Add Known Hosts manager and SSH folder import 2026-09-11 00:00:38 +08:00
Kaede Akino f0fdcf4ddb Sync Vault and settings across devices 2026-09-10 23:15:25 +08:00
Kaede Akino 526f8bdf07 Add get_metrics tool for Agent and MCP 2026-09-10 22:20:57 +08:00
Kaede Akino 3e0479f573 Refresh focused screenshots and showcase server metrics 2026-09-10 22:03:09 +08:00
Kaede Akino c0c63a0063 Refresh app icons and website branding 2026-09-10 21:17:27 +08:00
Kaede Akino 7caf70f5eb Add server metrics and menu bar monitoring 2026-09-10 20:47:35 +08:00
Kaede Akino e4f3e21615 Fix model role localization and skill wording 2026-09-09 20:58:52 +08:00
Kaede Akino e2490285c9 Remove unused Show Recents setting 2026-09-09 20:46:57 +08:00
Kaede Akino 8cdb3cbd44 Bump to 1.5.4(5) 2026-09-09 17:52:22 +08:00
Kaede Akino 6b240594b9 Add MCP client setup command previews and copy buttons 2026-09-09 17:36:36 +08:00
Kaede Akino 2afc70a285 Normalize fetched model types with AgentKit 0.6.0 2026-09-09 17:14:19 +08:00
Kaede Akino d2f3a80d8d Fix localization 2026-09-09 17:02:03 +08:00
Kaede Akino 58d2842a9e Fix zsh integration 2026-09-09 12:33:32 +08:00
Kaede Akino d8b12d98af Add theme selector 2026-09-09 11:47:18 +08:00
Kaede Akino 6a912cc5d0 Fix App Icon 2026-09-09 11:34:51 +08:00
Kaede Akino 3511bf963a Refactor free trial 2026-09-09 01:20:42 +08:00
Kaede Akino dd30b19391 Fix apple speech crash 2026-09-06 19:31:45 +08:00
Kaede Akino 9153b36390 Update model catalog 2026-09-06 11:59:26 +08:00
Kaede Akino f61a2cfc95 Bump to 1.4.2 2026-09-04 11:34:35 +08:00
Kaede AkinoandClaude Opus 5 ea3aae9b52 Address a provider by one URL and one API type
The API Type picker named three vendors — OpenAI, Anthropic, Google — for
four wire protocols, so `.openAI` carried a Responses toggle to express the
fourth. Each kind also carried a default base URL, which the editor printed
as the Base URL placeholder, so `api.openai.com` was on screen for anyone
who had added a provider and typed nothing, and a provider with a key but no
address quietly resolved to a vendor endpoint nobody had named.

The picker now names the protocol: Automatic, Chat Completions, Responses,
Messages, generateContent. Automatic reads the protocol off the URL's own
suffix and falls back to Chat Completions when nothing matches, which is
what an endpoint that does not name itself is almost always serving.

Base URL and API Path collapse into one field. Splitting them made "does the
base URL include /v1" a question the user had to answer, and it is the
question every client of this shape gets wrong. Paste whatever the gateway's
documentation printed, at whatever depth it printed it; the two captions
underneath say what that produced — Full URL is read from the same value the
request is built from, and the line under it names the body shape, which is
how Automatic explains its own answer and how an explicit pick that
disagrees with the URL stops being a silent surprise. An explicit pick moves
the format, never the address: the person who pasted it keeps it.

Vertex AI goes with the vendor names. It stays in AgentKit, which is an
independent package and does not carry this app's constraints, but nothing
here drives it any more — the editor's project, location and service-account
import are gone, along with five persisted columns and their Keychain path.

`ProviderEndpoint` holds the two things the editor owns, the typed URL and
the picked type, because neither survives a round trip through the composed
result: a URL ending in /messages looks the same whether it was typed or
appended, and Automatic looks the same as picking the format it resolved to.
`AIConfigurationModel.setEndpoint` is the only writer of the derived
`inferenceURL`, `baseURL` and `apiFormat`.

Old rows fold back to the address they used to resolve to rather than to
Automatic, so a provider that worked before the change points at the same
place after it. That fold needs the pre-V12 columns, which is why `kind`,
`baseURL`, `apiPath` and `usesResponsesAPI` are still declared, and it needs
V11 frozen: every VersionedSchema in the plan points at live model types, so
editing a persisted model rewrites what the old version *means* and leaves
an installed store recording a version the plan no longer contains. That is
a refusal to open, not a migration. `TermindSchemaV11Models` is what gives
V11 to V12 two sides — any future attribute change needs the same freeze.

Requires AgentKit 0.5.0, where `ModelAPIFormat` replaces `ModelProviderKind`
and a provider carries `inferenceURL` and `baseURL` instead of a kind, a
base and a path.

Verified with the full test suite (994 tests, including the new
ProviderEndpointTests), swift-format lint --strict,
Scripts/check-zh-localization.sh, npm run build, and a launch of the app
against an existing V11 store.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 11:33:34 +08:00
Kaede Akino 9bd4ac3029 Bump to 1.4.1 2026-09-03 15:19:10 +08:00
Kaede Akino 8a92004da3 Fix Authentication Request in Menu 2026-09-03 15:18:41 +08:00
Kaede AkinoandClaude Opus 5 a3f0a08919 Give Termind's own menu items icons
macOS supplies menu icons for standard AppKit actions and nothing else, so
every menu mixed system items that had one with Termind commands that did
not — Settings sat between About and Hide with a blank icon column, and the
File menu's Quick Open, Connect and New Host sat beside an iconed Close
Window.

`MenuSymbol` names one glyph per verb, in DesignSystem beside the other
presentation maps. The menu bar, the titlebar's New pull-down, the SwiftUI
context menus and the AppKit `NativeMenuEntry` menus are built by four
different mechanisms; reading their glyphs from one vocabulary is what keeps
Copy the same picture in all four. Values reuse what the app already draws —
`gearshape` from `AppCommandRegistry`, `apple.terminal` and `plus.bubble`
from the command catalog, `curlybraces` and `key.fill` from the Vault pages.

`NativeMenuEntry` gained a `symbol`; `WindowToolbarCreationAction` gained a
required one, since a new verb is drawn both as a SwiftUI item and as a
native entry and would otherwise lose its glyph in one of the two. Icons are
deliberately withheld where a menu's icon column reads as a group: the
trailing items of same-verb runs, and the tag toggles in the Hosts filter,
which are data rather than verbs.

Destructive items lose their hand-painted red, on both the SwiftUI and the
AppKit side. It was ours rather than the system's, so it did not follow
appearance, selection highlight or a disabled item, and it made Delete the
loudest thing in menus it merely happens to end. `ButtonRole.destructive`
says what the item is; AppKit had no equivalent, which left
`NativeMenuEntry.isDestructive` doing nothing, so it is gone.

Submenu children now read on from their parent — "Connect > with SSH",
"Duplicate > in Current Workspace" — and the status bar's Open with menus
join them rather than spelling the placement their own way, which retires
the "Current Workspace" and "New Workspace" catalog keys. Assistant's rule
moves above Archive: Archive and Delete both take a chat off the list, while
Pin and Rename only change how it is listed.

Verified with the full test suite (985 tests), swift-format lint --strict
and Scripts/check-zh-localization.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 15:15:45 +08:00
Kaede Akino 4a5e52488c Bump to 1.4.0 2026-09-02 19:09:50 +08:00
Kaede AkinoandClaude Opus 5 9bd2401a5c Add Open at Login
Settings › App › Startup gains an Open at Login switch backed by
SMAppService.mainApp, and an Open Hidden at Login switch that keeps the
login launch from taking the screen. Both are off by default.

The registration is not mirrored into preferences. SMAppService is the
store for whether Termind opens at login: the user can revoke it in
System Settings without telling the app, so the switch re-reads the
system status on appear and on every window activation. requiresApproval
counts as on, with a row saying what is outstanding and a button into
Login Items, rather than a switch that quietly refuses to hold.

Hidden means NSApp.hide(nil) rather than a suppressed window, because
Termind terminates with its last window. LoginLaunch latches the launch
Apple Event during applicationWillFinishLaunching, the only moment
currentAppleEvent can still be read; the hide is armed a second time for
the window SwiftUI orders front just afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:07:54 +08:00
Kaede AkinoandClaude Opus 5 b13c87cbe8 Move splits and Workspace navigation off keys macOS already owns
Split Right held ⌥⌘D, which macOS reserves system-wide for Turn Dock
Hiding On/Off as symbolic hotkey 52. The system consumed the event
before the menu saw it, so the command was unreachable from the
keyboard and pressing it only hid the Dock. It and Split Down move to
⌥⌘→ and ⌥⌘↓, the directions they name. ⌥ still marks the pair that only
rearranges the window, against Duplicate's ⌘D, which opens a
connection.

Next Workspace and Previous Workspace held ⌃⇥ and ⌃⇧⇥. The app never
sets `allowsAutomaticWindowTabbing` to false, so AppKit installs its own
Show Next Tab ⌃⇥ and Show Previous Tab ⌃⇧⇥ into the same Window menu
and shadows both.

Those two were also exact duplicates. `Show Next Workspace` invoked the
same `selectNext()` as `Next Workspace`, and the Previous pair likewise
— four buttons for two actions, where the shadowed pair held the better
titles. The Window menu keeps one pair on ⇧⌘] / ⇧⌘[, which is what
Safari, Terminal and Finder use, under the names Next Workspace and
Previous Workspace. Both already carried finished zh-Hans, so no new
translation was needed and the two orphaned catalog entries are gone.

`AppShortcutKey` gains `.rightArrow` and `.downArrow`. They match on key
code rather than character, as `.tab` and `.escape` do, because that is
what lets `AppShortcutRegistry.routesToApplication` recognise ⌥⌘→ inside
the terminal's `performKeyEquivalent` and decline it — otherwise Ghostty
encodes the arrow and writes an escape sequence to the shell.
`registeredApplicationShortcutsFallThroughTheTerminal` now covers both.

Verification: build succeeds, 975 of 975 tests pass, and
`swift-format lint --strict` and check-zh-localization.sh are clean. The
menu bar itself was not driven: osascript has no assistive access here,
so rendering of ⌥⌘→ and a live keypress inside a focused terminal remain
unchecked by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:34:43 +08:00
Kaede Akino 2b7908de6a Bump to 1.3.0 2026-09-02 18:10:28 +08:00
Kaede Akino ba46e90e30 Format with swift-format
`swift-format lint --strict --recursive Termind TermindTests
TermindMCPBridge` is in the contribution instructions but had never been
run to completion: 4133 violations across 141 files, most of them
indentation and line breaking the pretty-printer would have settled on
its own.

`swift-format format -i` accounts for all but five. The rest are
lint-only rules it will not rewrite for you:

- `SSHKeyMaterial` declared its libssh2 entry point in the C symbol's
  own spelling. `@_silgen_name` already carries the linkage name, so the
  Swift side is free to be `libssh2PublicKeyFromPrivateMemory`.
- Four `.forEach { ... }` calls become for-in loops, two in
  `PortForwardingController` and two in tests.

Formatting only, and checked as such rather than assumed: all 5409
string literals under `Termind` are byte-identical across the change, so
no localization key moved. Comparing whitespace-stripped token streams
leaves 43 files with any real change, and each is one of the rules
above, a dropped semicolon, a `case a, b` split onto separate lines with
its raw values kept explicit, or a file-scope `private` respelled
`fileprivate` — which is the same access level in that position.

Builds, and the suite stays at 975 of 975.
2026-09-02 18:09:30 +08:00
Kaede Akino 41404ca48d Fix flaky and never-green tests
The suite never finished. A SIGSEGV around 272 of 975 tests killed the
runner, so xcodebuild only ever reported one shard and roughly seventy
percent of the tests were not being executed at all. Underneath it sat
four failures, two of which had been red since the day they were
written, and three tests whose outcome was decided by state they did not
own.

The crash: `RemoteWindowTitleTests` closed a window whose
`isReleasedWhenClosed` still defaults to true, releasing one ARC also
owned. The second release landed on freed memory during an autorelease
pool drain, surfacing as `-[_NSWindowTransformAnimation dealloc]` and
charged to whichever test happened to be running. Closing it also risked
terminating the test host, which is the real application and whose
delegate quits once the last window closes. Ordering the window out
instead settles both.

Never green since written:

- `conversationOwnedImportHonorsAttachmentLimitBeforeStartingTasks`
  staged twenty chips that differed only by label, and `attach`
  deduplicates on kind and content, so one went in and the limit under
  test was never reached.
- `failedCompactionSaveDoesNotChangeTheInMemoryContext` asked for a cut
  keeping two tokens after a last turn worth one, so the search walked
  past it and `compactionSpan` returned nil. Both turns are now sized in
  the unit the cut is chosen in, sharing `filler(tokens:)`.

Stale or ambient:

- `ColorSystemTests` read the domain value `colorTag: .blue` as a
  display colour. The scan predates tag colours by a day.
- The Chinese text for a finished run changed in 0b67497; the assertion
  did not follow.
- `conversationOwnedFileImportFinishesAndKeepsFilename` yielded rather
  than slept, which does not wait for a read off the main actor.
- `shiftedPunctuationMatchesItsUnshiftedShortcutKey` rests on AppKit
  re-deriving a character through the current input source, which it
  declines for a synthesized event about a third of the time. It now
  asserts when AppKit answers and stands down when it does not.
- `FileURLDropTargetTests` reached `hitTest`, which reads
  `NSApp.currentEvent` and the machine-wide drag pasteboard for itself.
  That decision moves to `participates(at:currentEventType:pasteboard:)`
  with both inputs passed in; `hitTest` keeps its behaviour and the
  tests keep their own pasteboard.

Verified with eight consecutive full runs at 975 of 975.
2026-09-02 18:07:34 +08:00
Kaede AkinoandClaude Opus 5 3115e202c7 Draw Keychain key editors on the form background
The private key and certificate fields kept TextEditor's own opaque
control background, which inside a grouped Form reads as a white card
sitting on the section rather than as part of it. Hiding it puts them on
the same footing as the skill editor's fields.

Fix the prompt alignment that removing the box exposed. The overlay
carried `.padding(.top, 8)`, but TextEditor insets its text container by
its line fragment padding horizontally and by nothing vertically, so the
prompt stood a line below the caret it was standing in for. Measured
offscreen: leading 5, top 0 — the same inset the composer prompt in
ConversationViews uses, now a named constant.

Both sections repeated their own title in a row above the editor next to
the Choose button. With no box left for that row to attach to it read as
a second heading, so the button moves to the trailing edge of the section
header, matching the Targets section in the snippet editor. The
certificate editor gains the accessibility label its removed label row
had been standing in for, and the private key editor gains a prompt,
since an empty unboxed field is otherwise an unmarked gap in the form.

Verification: build succeeds; check-zh-localization.sh passes;
swift-format reports only the findings this file already had on main.
The failing suites in the test run (ChatComposerStateTests,
ColorSystemTests, WindowStateTests) fail identically on a clean HEAD and
are untouched by this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 16:14:49 +08:00
Kaede Akino cba40eb06b Better localize 2026-09-02 16:01:30 +08:00
Kaede AkinoandClaude Opus 5 68dee1137d Stop disabled drop targets from claiming file drags
On the Assistant page, dragging a file onto the sidebar attached it to
`window.sidebar.assistantDraft` — the Workspace sidebar's new-chat draft,
which is not on screen there, so the file appeared to vanish.

`AssistantPanelView` already declared the intent with `isEnabled:
isFocusedOnWorkspace`, but the flag never reached AppKit.
`FileURLDropNSView` registered its dragged types unconditionally in `init`
and never unregistered, and only `hitTest` consulted `isEnabled` — the
three dragging callbacks did not. A view that refuses the hit test but
stays in the window's registered destination list still wins the drop.

This is the asymmetry `AssistantConversationListContainer.isInteractive`
records for conversation drags; the shared file receiver had only the
hit-test half. Give it both: follow `isEnabled` when registering and
unregistering, and gate `supportedPayload(from:)`, the one chokepoint
`hitTest` and all three callbacks pass through. The callbacks already
treat a nil payload as "not mine", so their bodies are unchanged. The
second half is what holds when `isEnabled` flips during a drag already in
flight, after AppKit has resolved its destinations.

Fixing the shared view also covers two sites with the same flaw: hidden
Workspaces, which all stay mounted at `opacity(0)` over the same frame
carrying a pane drop target, and Keychain accepting drops mid-load.

`FileURLDropTargetTests` becomes `.serialized` — every test there stages
its case on the one drag pasteboard the system shares, and the suite was
already racing with itself before these two tests made it fail.

Verification: macOS build, full test suite (remaining failures match the
clean-tree baseline and are unrelated), swift-format lint adds nothing
new, and Scripts/check-zh-localization.sh passes. The end-to-end Finder
drag is still to be confirmed by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:44:52 +08:00
Kaede Akino 1c429a301b Adopt AgentKit 0.3.0 scratch_replace, scratch_copy and scratch_move
scratch_edit is now scratch_replace, and takes a regex mode and a per-entry
count — omit it to require exactly one match as before, 0 for every occurrence,
n for the first n. scratch_copy and scratch_move move a staged file or subtree
without its bytes passing through the transcript, which is what duplicating a
file before a risky change used to cost.

The app-side changes are the ones the rename and the two new tools reach:
ActionKind.copy in both the English and Chinese verb tables, the pull → copy →
replace → push recipe in the built-in config-edit skill, and the same wording in
the system prompt and the plan contract. The legacy tables are deliberately
untouched: legacyToolNames and legacyMetadata describe cards written before
presenter IDs were persisted, and scratch_edit belongs in both. AgentKit keeps
builtin.scratch_edit registered as an alias, so a transcript from before the
rename still draws its diff rather than falling back to generic fields — which
aCardWrittenAsScratchEditStillFindsItsProjection now pins.

AppLanguageTests no longer requires %lld results. That plural moved to AgentKit
with web_search's card, which is what db8796c removed from this catalog; the
test's list had been left behind and was failing.

AGENTS.md said AgentKit was referenced as a local SPM package. It is a versioned
remote dependency, so an uncommitted change in ../AgentKit is invisible to the
app and a change spanning both sides needs a tagged release in between. Said so,
because working the other way wastes a build cycle before the mistake shows.

Verified: AgentKit 0.3.0 builds and passes its 112 tests, and the six suites
this touches pass here. The five failures in the full run — ColorSystemTests,
two ChatComposerStateTests, FileURLDropTargetTests and WindowStateTests — all
reproduce on a clean worktree at HEAD and are unrelated.
2026-09-02 15:44:52 +08:00
Kaede Akino 9711a35841 remove stale localizable 2026-09-02 15:44:52 +08:00
Kaede Akino 6d17fef156 update libghostty-spm to 1.5.1 2026-09-02 15:44:51 +08:00
Kaede Akino cdd8fe0668 Extract the agent runtime as AgentKit
The loop, the approval gate, the provider adapters, the MCP client, and the
tools with no opinion about transports now live in a package and are consumed
from it. Termind keeps everything that reaches a machine or a person: the SSH
and SFTP tools, the connection pool, the Vault resolver, scratch_pull and
scratch_push, the SwiftData store, and every view.

They meet at four seams. TermindAgentTools registers our tools alongside the
built-in groups; AgentToolServices carries the SSH credential prompt to them
without that prompt appearing in a runtime protocol; TermindAgentPolicy owns the
system prompt, the plan contract, and the ssh_exec plan-mode rule the runtime
cannot write for us; TermindAgentPresentation extends the card vocabulary with
our own semantics.

ChatPermissionMode becomes AgentPermissionMode and moves out of the conversation
model — who authorizes an action is a property of the run, not of a view.
AgentRunRequest takes the session context as rendered text rather than our
SessionContext, because the runtime does nothing with it but insert it in front
of the turn it describes.

Termind's system prompt and plan contract are byte-identical to what shipped.
Every provider cache is a prefix cache, so editing a word there would miss the
cache on every replayed conversation at once.
2026-09-02 13:58:35 +08:00
Kaede Akino 4c05250cad Rename in SSH/SFTP to with SSH/SFTP 2026-09-02 11:08:38 +08:00
Kaede Akino f159082299 Add Agent Skill 2026-09-02 11:08:38 +08:00
Kaede Akino 9e1264455e refactor 2026-09-02 09:57:52 +08:00
Kaede Akino 9309a425d3 Reduce redundant production and copy test logic 2026-09-02 09:34:35 +08:00
Kaede Akino 54ea515750 unified ProAccessControllerError 2026-09-02 07:34:14 +08:00
Kaede Akino dc4bd34414 Add review mode and remove developer pro 2026-09-01 23:27:58 +08:00
Kaede Akino 7128b64681 Bump to 1.2.2 2026-09-01 10:48:59 +08:00
Kaede Akino 2f96b5a75a Fix Shell Integration 2026-09-01 10:47:13 +08:00
Kaede Akino 1449bd62ac Bump to 1.2.1 2026-08-31 22:03:31 +08:00
Kaede Akino 9f3f1e40d9 Refine drag UI 2026-08-31 22:03:05 +08:00
Kaede Akino f125f72070 Refine Shell Integration 2026-08-31 21:57:54 +08:00
Kaede Akino 18c6f08947 Add AGENTS.md and CLAUDE.md 2026-08-31 12:29:38 +08:00
Kaede Akino 8e55b12e4a Bump to 1.2.0 2026-08-30 17:27:00 +08:00