Skip to content

Use core v0.58.0: session conversations, unified guards, script plugins - #270

Merged
fylorn merged 27 commits into
devfrom
release-pin
Oct 3, 2026
Merged

fylorn merged 27 commits into
devfrom
release-pin

Conversation

@fylorn

@fylorn fylorn commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

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

  • A session's sheet has two tabs, Summary and Conversation. Conversation replays the session turn by turn from core's SessionTranscript.
  • Each turn shows its messages by role: text, folded thinking, tool calls with a one-line preview, tool results, and images as chips. System prompt changes and restarts after compaction are marked.
  • What could not be shown is a short note: past the retention period, too large to keep whole, unreadable, or why the turn failed. A turn opens its request in the drawer.

Security: three guards

  • Outbound redaction, tool-call inspection and the content filter. Hidden characters are now built-in content rules, and the output limit is gone.
  • The enforcing mode is named after what each guard does: Replace, Cut off, Enforce.
  • Content rules can refuse, delete or only record, and match by text, regex or code points.
  • Custom redaction rules have a placeholder name. Email addresses and Chinese mainland mobile numbers join the built-in rules.
  • Test dialogs show what is sent after replacing or deleting. The log has a Deleted outcome, and Traffic rows a Deleted badge.

Plugins

  • A new Plugins page lists the installed plugins in run order, with status, permissions, scope and run statistics.
  • Add a plugin from a .js file 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.
  • Settings, trial run on a recent request, logs, reordering and deleting.
  • Traffic and the request drawer show which plugins ran and what they changed. Plugin failures reach the notification bell.
  • Core adds its two default plugins on first start, turned off: Answer in a chosen language, and Convert WSL and Windows paths.

Merged branches

This PR's own commits

  • All core crates pinned to v0.58.0. Cargo.lock re-locked with cargo metadata only: just the seven core crates move to c2a7bc6. tw-api now depends on tw-guard, and tw-guard on bytes and ts-rs, all already in the lock.
  • src/generated/tw-api.ts already matched the tag: regenerating changes nothing.
  • Core removed the deepseek-flags default plugin. Its names, description and tests are gone; the notice test uses wsl-paths instead.
  • The note in src/i18n/plugin-defaults.json now says core's manifest labels are English. The table matches core's two defaults: ids, English names, setting keys.
  • scripts/shots/core/oracle.sh against v0.58.0: only the version in the status answers changes.

Upgrade notes for the next release

  • A remote core must be 0.58.0 (protocol 34).
  • Core rebuilds the request store (schema 25): the request history is cleared on first start.
  • security.hidden_text and security.output_limit are removed. A config.yaml that 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 build
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test, including ts_bindings, msg_codes, the webview whitelist and control_plane against the published twcore v0.58.0
  • scripts/version.sh, scripts/release_notes_test.py

🤖 Generated with Claude Code

fylorn and others added 27 commits October 2, 2026 17:29
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>
@fylorn
fylorn merged commit 2d2e110 into dev Oct 3, 2026
4 checks passed
@fylorn
fylorn deleted the release-pin branch October 3, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant