Use core v0.58.0: session conversations, unified guards, script plugins - #270
Merged
Merged
Conversation
The resource cache never evicts: fine for the small data it held so far, but a session's transcript can be several megabytes. `forget(key)` drops an entry nobody is subscribed to and nothing is fetching, so a view that caches big payloads can keep only the last few. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session sheet gets two tabs, Summary (the existing totals, input per turn and cost per turn) and Conversation: the session replayed turn by turn from core's SessionTranscript. Each turn has a header with its number, time, model, cost and outcome, joined from SessionDetail by request id (the transcript's ids are strings, TurnView's are numbers), and a "Request details" button that stacks the request drawer on top. Inside a turn, a fixed role column (User / Assistant / Tool / System) and the content: plain text with whitespace kept, thinking folded, tool calls with the name and a one-line preview (arguments folded, pretty-printed JSON), tool results folded with an error state, images and other parts as chips. The system prompt sits folded at the top; a system prompt change is a folded row on that turn; a restart (e.g. a compacted context) gets a divider and folds the history the request carried up to the model's last message. Gaps are short inline notes: past the retention period, too large to keep whole, unreadable, or the turn's failure reason. Runs of turns past retention fold into one row; calls that generate no answer (count_tokens, compaction) are a single header line; turns still in flight close the list. The transcript is fetched when the tab is first opened and refetched only when SessionDetail records a new turn; unchanged turns keep their objects so memoised turns do not re-render, and only the last three sessions' transcripts stay cached. Long transcripts render in batches (no long task for 600 turns), long text and long tool output are clipped with "Show all", and folded content is not in the DOM. Switching tabs keeps the conversation's scroll position and expanded rows. Core's endpoint is not released yet: its types and fetch live in src/traffic/transcript.provisional.ts with the swap steps for integration. The whitelists in src/control.ts and src-tauri/src/call.rs cannot list SessionTranscript before the generated types have it; both carry a TODO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Installing a plugin, replacing its code and approving a changed file now go through three Tauri commands (plugin_install, plugin_replace_source, plugin_approve). Each one re-reads the code through core's PluginInspect instead of trusting what the webview says about it, shows a native OS dialog with the plugin name, its permissions in plain words and the SHA-256 prefix, and only then calls CreatePlugin, ReplacePluginSource or ApprovePluginFile. Cancel is the default button, Esc cancels, and only one confirmation can be open at a time. macOS uses NSAlert, Windows MessageBoxW and Linux a GTK MessageDialog, all already linked by the app, so there is no new dependency. Plugin names are stripped of control, zero-width and bidi characters before they reach the dialog. The other plugin endpoints join the webview whitelist through a provisional group in call.rs, described in plugins/wire.rs until core ships them in tw-api. CreatePlugin, ReplacePluginSource and ApprovePluginFile stay out of it, and a test says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new Plugins page after MCP lists the installed plugins in run order, each with its status (active, disabled, file changed, failed to load), permission chips (tool calls in replies in red), scope and run statistics, and its actions written as words: Settings, Trial run, Logs and Delete, plus Review changes or Replace code when the file changed or cannot load. Enabled plugins that are not running and reject requests in their scope get a banner until they are dealt with. - Add: choose a local .js file or paste code, then review the full code (read-only CodeMirror with a small JavaScript highlighter, long lines wrapped and invisible characters marked), every permission with its consequence, syntax errors with line and column, the ID, scope, settings and what to do on error. Install hands over to the native confirmation. - Review a changed file: line diff against the approved copy, new permissions marked, then the native approval. - Settings, trial run (a recent request in the plugin's scope, before/after diff for the request and reply, logs), logs, reorder and delete. - Plugin-provided strings are rendered as plain text, isolated with <bdi>. Shortcuts: there are ten pages now, so Cmd/Ctrl+1 to 9 cover the first nine pages other than Settings, and Settings is Cmd/Ctrl+, only. The plugin types are provisional (src/plugins/api.provisional.ts) until core ships them in the generated tw-api.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Requests that a plugin changed carry a Plugin mark in the Traffic list. In the request drawer, the Timeline lists every plugin run (plugin, request or reply, outcome, CPU time, error), and the Payload tab switches between a comparison, the original request and the request after plugins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
core's plugin_failed event becomes a warning notice that opens the Plugins page. The notice names the plugin and the request but never repeats what the plugin said, since system notifications show on the lock screen. Each failure counts as a new event, so a plugin that fails on every request is merged by the cooldown instead of flooding. The pinned tw-api does not know the event yet, so the event stream hands events it cannot parse to a provisional callback that recognises plugin_failed. The zh table has a placeholder section for the gw.plugin.* codes until core defines them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swap the provisional module for the generated contract: - src/generated/tw-api.ts is regenerated from tw-api at ThinkWatch-Core main 33b6ae9 (#251 session transcript, #252 stored bodies): CONTROL_API_VERSION 32, the Transcript* types and the SessionTranscript endpoint. v0.58.0 will be tagged from that main, so this is the file the tag generates. - SessionTranscript joins both webview whitelists (src/control.ts and src-tauri/src/call.rs) and the screenshot mock's CORE. - src/traffic/transcript.provisional.ts is gone: the types come from @/types and the conversation is fetched with call("SessionTranscript", null, id). Not in this commit: the tw-* pins in src-tauri/Cargo.toml still say v0.57.1, because v0.58.0 is not tagged yet. Until they move to v0.58.0 (with cargo update, and fetch-core.sh fetching the matching twcore) the Rust side does not build against the pinned core: call.rs names ep::SessionTranscript. Checked locally with a temporary [patch] pointing the six core crates at core main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conversation said "past the retention period" for every turn whose request or response body is missing. Core deletes bodies by age, but a body can also be missing well inside the period: the total-size cap deletes the oldest days first without waiting for them to expire, core drops a body rather than make a request wait when writes fall behind, and bodies deleted before the period was lengthened do not come back. missingWhy() decides from the turn's time (SessionDetail's at_ms) and retention.body_days from the overview (the cached resource the request drawer already uses). Older than the period keeps the retention wording; anything else, including an unknown time or period, gets a neutral line that names no cause, such as "This turn's content was not kept" and "The response body was not kept" (the Chinese says the same; see the unkept* keys in Conversation.i18n.tsx). Runs of turns with nothing to show are folded only when they share the reason, and the empty state says "past the retention period" only when every turn is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rawer too
Core now records an error the upstream answered and the gateway passed
on as a failed request, with the upstream's own words, and TurnView
carries the status (ThinkWatch-Core#263, merged as 104559c on main; the
next tag, v0.58.0, is cut from it). Until then a 400 such as "prompt is
too long" looked successful in session detail, and the conversation
showed only the user's message for that turn, with no answer and no
reason.
- src/generated/tw-api.ts is regenerated from core main 104559c:
TurnView.status (number | null) and the docs of HistoryRow.error and
Summary.failed / no_usage_requests. CONTROL_API_VERSION is still 32.
- core.zh.json translates the new message code gw.upstream.status_message
("上游「{upstream}」返回 {status}:{message}"), with a shared case.
- Conversation: a failed turn shows one line where the answer would be,
core's reason as it is (failureLine in transcript.ts), for example
"上游「中转」返回 400:prompt is too long: …" / "Upstream `中转`
answered 400: prompt is too long: …". The "请求失败:" / "The request
failed:" prefix is gone; the turn header already says Failed. When the
turn has a non-2xx status the reason does not carry (its `status`
argument), the status goes in front: "上游返回 400:…" / "The upstream
answered 400: …".
- Request drawer: the hover text of "Not saved" said "This record is past
its retention period." for every missing body, which is wrong for
WebSocket and locally answered requests (they never store bodies) and
for bodies dropped inside the period. It now uses the conversation
view's rule (missingWhy: the request's time against
retention.body_days from the overview): past the period keeps that
sentence, anything else says "The request body was not kept." / "The
response body was not kept." (请求正文未保留。/ 响应正文未保留。).
- Tests: failureLine (zh and en: the upstream's words, status only, a
status the reason does not carry, failures that never reached an
upstream or broke after a 2xx, no failure) and notSavedTip (past the
period, inside it for request and response, period unknown, English).
- The screenshot mock's turns carry the status.
The tw-* pins in src-tauri/Cargo.toml still say v0.57.1. The Rust side
was checked with a temporary [patch] pointing the six core crates at
core main 104559c (not committed), and a twcore built from it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ints and placeholder names The security page follows the guard-unify contract: hidden characters become a group of built-in content rules and the output limit is gone, so the page has four tabs (log, outbound redaction, tool-call inspection, content filter). - The third mode is named after what each protection does: Replace, Cut off, Enforce. The page header counts them together as "enforcing". - Content rules can refuse, delete or only record, and match by contains, regex or code points. The rules table groups hidden characters first and has an Action column; code-point rules show their ranges. - The custom content rule dialog adds the code-point match (checked as typed) and the Delete action; the built-in dialog lets the action be changed and states the factory setting. - Custom redaction rules have a placeholder name, SECRET filled in by default, checked as typed, with the resulting <<TW_NAME_1>> shown next to it; the built-in dialog states what a rule is replaced with. Email and Chinese mainland mobile numbers join the built-in catalog. - Test dialogs send the action picked in the dialog and show what is sent after replacing or deleting, and say when the content filter would refuse the request. Invisible characters in a highlighted match are drawn as code points. - The log has a Deleted outcome; code-point hits show the character count and the hidden text they spelled. Traffic rows get a Deleted badge. The overview's security section has three rows. Core is not released yet: the new shapes live in src/security/api.provisional.ts and shadow the generated ones through src/types.ts and src/control.ts until tw-api.ts is regenerated. The removed SetSecurityLimit endpoint is dropped from both webview allow-lists. The Chinese table drops the hidden-text and output-limit sentences and adds provisional codes for the new ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core's first stage generated the shared rule view and trial types; the provisional ones now match them field by field. A run of hidden characters in a highlighted match is drawn the way core writes it in excerpts (‹U+E0049 ×12›), so the log recognises custom code-point hits by that form too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nal message codes src/generated/tw-api.ts is regenerated from core feat/guard-unify (414759e), and the provisional types are gone. The Cargo.toml pin is unchanged (v0.57.1): the bindings and message-code tests will match again once the release tag is pinned. - Content log entries and content_matched events say how the rule matches (match), so code-point hits are recognised from that instead of from the rule table or the excerpt. - Tool-call inspection has two built-in rules implemented in code (a credential sent to an unknown host, a local file uploaded to an external host). Their Match cell and dialog say what they check, and since there is no pattern to copy they offer no "Copy as a custom rule". Names and reasons are in the Chinese table next to the other tool-call rules. - The Chinese table follows core's final codes: config.rule_codepoints_bad, config.rule_label_bad, security.pattern_empty and gw.upstream.status_message are added; the tool-call cut messages are renamed to gw.toolcall.response_cut, gw.toolcall.response_withheld and gw.toolcall.connection_cut and no longer name the upstream. - The notices snapshot fixture and the screenshot pipeline's recorded core answers drop hidden_text and output_limit; the latter were regenerated with oracle.rs against the same core commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hidden characters are part of the content filter now, which can delete what it matches as well as refuse, and the third mode is named per protection, so the protection bullet no longer says "refused as well" or "switch to Enforce". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…failures
Rust side of the Plugins UI, now built against core's feat/plugins
(CONTROL_API_VERSION 33) instead of the provisional wire.
- The plugin endpoints come from tw_api::ep. The provisional
plugins::wire module, the provisional `call` group and the raw
event fallback are gone; plugin_failed is Event::PluginFailed.
- plugin_update_confirmed: re-reads the plugin through core
(re-inspecting its approved source when core has no manifest for it),
shows a native confirmation with what is changing (turning on,
settings, scope, on_error) and what the plugin can do, then calls
UpdatePluginConfirmed. The endpoint stays out of ALLOWED and
WEBVIEW_ENDPOINTS, with a test.
- Native install / replace / approve dialogs name the extra request
kinds a plugin handles ("Also handles: embeddings, completions").
- Default plugins (reply-language, wsl-paths, deepseek-flags) are named
in the UI language in native dialogs and notices, from
src/i18n/plugin-defaults.json, which the UI reads too. They are
matched by id and by the manifest name core ships.
- Notices: plugin_failed is gathered per plugin. The first failure goes
through the bus at once; further ones within 10 s are merged into one
ingest with the real count, so a plugin failing on every answer
(gw.plugin.reply_busy) no longer writes the list to disk, pushes it to
the UI and re-posts the notification each time. The body names core's
own reasons (limits, overload, changed file) and never plugin text.
- Chinese for every config.plugin.*, control.plugin.* and gw.plugin.*
code on feat/plugins; "plugin" in the kind table.
- Regenerated tw-api.ts and lite-api.ts (the native command types moved
into wire.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The provisional module is gone: plugin calls go through `call`, types
come from the generated tw-api.ts / lite-api.ts, and errors, statuses
and run errors are core messages shown through the zh table.
- Guarded updates: turning on, or changing the settings or scope of, a
plugin that holds reply_tool_calls (or whose permissions core cannot
read) goes through plugin_update_confirmed; a 403
control.plugin.needs_confirmation from UpdatePlugin is retried through
it. Such a switch does not flip until the native dialog is confirmed;
a cancelled dialog leaves the switch, the settings form and the
config as they were (`DECLINED` in `undoable`).
- Default plugins show Chinese names, descriptions and setting labels
(English UI: English setting labels), matched by id and manifest name.
- "Also handles: embeddings, completions" in the row and in both review
dialogs when a plugin handles more than conversations.
- The scope editor always offers all three parts and says that models
match the model sent upstream after routing rewrites it, and that
upstreams apply to requests and replies. Trial candidates match the
same way, case-insensitively.
- RequestDrawer groups plugin runs by attempt when there was more than
one, and says which attempt sent the after-plugins body; when the
answering attempt got the original there is only the original.
- A plugin whose manifest core cannot read shows its id and status only.
- String settings are auto-growing multi-line text areas.
- Config history labels the `defaults` origin ("Default plugins").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion retry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… update Against a real core: UpdatePlugin refuses to turn on the default wsl-paths plugin with control.plugin.needs_confirmation, UpdatePluginConfirmed turns it on, and turning it off stays on the webview path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security-guard unification UI (core #268): three protections, content rules that block, strip or record, code-point rules, the new built-in rules, hidden characters folded into the content filter and the output-limit tab removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session conversation view: core's SessionTranscript, failed turns with the upstream's reason, and missing-body wording that depends on the retention period. src/generated/tw-api.ts conflicted (both sides regenerated it from different core commits). It is not merged by hand: it is generated from core main da98022 (CONTROL_API_VERSION 34) with tw-api's export_ts example, the same tw_api::ts::typescript() that tests/ts_bindings.rs checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Plugins page, native confirmations for install, replace, approve and confirmed updates, plugin marks in Traffic and the request drawer, plugin failures in the notification bus, and Cmd/Ctrl+1-9 for the first nine pages with Settings on Cmd/Ctrl+,. Conflicts: - src/generated/tw-api.ts: generated from core main da98022 again (CONTROL_API_VERSION 34), not merged by hand. - src/i18n/core.zh.cases.json: both sides appended cases at the end; kept all three (gw.upstream.status_message, gw.plugin.setting_type, gw.plugin.reply_busy). - src/RequestDrawer.tsx: the payload tab keeps the plugin's RequestBody (original / after plugins / compare) and the response body's which/at; Body keeps the title-row extra and the retention-aware NotSaved, and RequestBody passes which="request" and the request's time to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/shots/core/oracle.sh's steps run against core main da98022, the content of the coming v0.58.0, instead of the pinned v0.57.1 tag. Only the status answer changes: api_version 33 -> 34. Rerun the script once the pin moves to v0.58.0; it should not change anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the Tauri event helper (#268): every subscription goes through subscribe() / useTauriEvent() in src/lib/tauriEvent.ts, which unlistens exactly once, also when the cleanup runs before listen() resolves, and catches Tauri's throw instead of leaving an unhandled rejection. No conflicts. The Plugins page, the conversation view and the guard UI added no direct listen() calls: they subscribe through useResource's events, i.e. useCoreEvent, which now uses the helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin every core crate to the v0.58.0 tag (c2a7bc6, control-plane protocol 34). The lockfile was re-locked with `cargo metadata` only, so nothing but the core crates moves; tw-api now depends on tw-guard, and tw-guard on bytes and ts-rs, all already in the lock. The generated bindings already match this tag. Rerunning scripts/shots/core/oracle.sh against it changes only the version in the status answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core v0.58.0 no longer ships it; reply-language and wsl-paths are the only default plugins. Remove its localized name and description from src/i18n/plugin-defaults.json and the tests that named it. The notice test now uses wsl-paths. Core's default manifests now write the setting labels in English too. Say so in the table's note and in the two readers' comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Moves ThinkWatch Lite to core v0.58.0 (c2a7bc6, protocol 34) and lands the three branches built against it.
What users get
Sessions: the conversation
SessionTranscript.Security: three guards
Plugins
.jsfile or pasted code after reviewing the whole code and every permission. Installing, approving a changed file, and turning on a plugin that can change tool calls go through the system's confirmation dialog.Merged branches
guard-unify-uiintegrate-transcript(session conversation view)plugins-uidevup to 8ab0dbc: fix: subscribe to Tauri events through one helper that unlistens safely #268 (subscribe to Tauri events through one helper that unlistens safely) and ci: run the frontend unit tests #269 (run the frontend unit tests in CI)This PR's own commits
v0.58.0.Cargo.lockre-locked withcargo metadataonly: just the seven core crates move to c2a7bc6. tw-api now depends on tw-guard, and tw-guard onbytesandts-rs, all already in the lock.src/generated/tw-api.tsalready matched the tag: regenerating changes nothing.deepseek-flagsdefault plugin. Its names, description and tests are gone; the notice test useswsl-pathsinstead.src/i18n/plugin-defaults.jsonnow says core's manifest labels are English. The table matches core's two defaults: ids, English names, setting keys.scripts/shots/core/oracle.shagainst v0.58.0: only the version in the status answers changes.Upgrade notes for the next release
security.hidden_textandsecurity.output_limitare removed. Aconfig.yamlthat still has either key does not load, and the app starts in safe mode. Lite 2026.10.0 writes them only when one of these settings was changed from its factory value.Checks
pnpm install --frozen-lockfile,pnpm typecheck,pnpm test(722 tests),pnpm buildcargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test, includingts_bindings,msg_codes, the webview whitelist andcontrol_planeagainst the published twcore v0.58.0scripts/version.sh,scripts/release_notes_test.py🤖 Generated with Claude Code