Commit Graph

3027 Commits

Author SHA1 Message Date
rcourtman 8d33986b5e Sanitize session-compaction transcript at the model boundary (privacy audit fix)
A systematic audit of all model-bound paths (not just the ones touched reactively)
found a leak: SummarizeSession (internal/ai/chat/session_compaction.go) sent the
PERSISTED chat transcript to the chat model via provider.Chat WITHOUT the
dial-aware model-boundary sanitizer. The transcript is built from persisted
messages — original user prompts and tool outputs — which carry raw resource
identifiers (hostnames/IPs/names) regardless of how the live turns were redacted.
On a cloud chat model at redacted/local_only, that shipped identifiers to the
provider, contradicting the cloud_context_privacy dial. (Secrets were already
redacted at transcript-build time via safety; resource identifiers were not.)

Fix: run the compaction ChatRequest through modelboundary.RequestSanitizerForModel
with the same dial resolution as a normal turn — fail closed to redacted when no
config snapshot, RedactLocalOnlyResourcesOnly() at full, nil (no-op) for local
Ollama. Mirrors the interactive seam exactly.

Audit also checked: report/fleet narrators, quick analysis, ExecuteAgentic(Stream),
discovery analysis, and Patrol all already install the sanitizer (verified). The
Patrol preflight self-test sends a fixed payload with no resource content, so it
needs no sanitizer (verified static, not trusted from the audit summary). agentcontext
output flows through the sanitized agentic loop. So compaction was the one gap.

Contract: ai-runtime gains a UNIVERSAL backstop rule — every model-bound path that
carries infrastructure-derived content must install the dial-aware sanitizer;
session compaction named explicitly; static no-identifier probes exempted. Proof:
TestServiceSummarizeSessionRedactsResourceIdentifiersForCloud (a transcript
hostname is stripped in the captured compaction request at the redacted dial);
existing compaction tests stay green (fix is additive — empty-model path unchanged).
Full internal/ai/... suite green (23 packages).
2026-06-08 22:10:49 +01:00
rcourtman bc5da3f4c5 Make the inventory resource context obey the cloud_context_privacy dial (increment 2 completion)
Increment 2 wired the dial into the prefetch and the model-boundary sanitizer but
missed a third model-bound path: the broad inventory context builder
(internal/ai/resource_context.go buildUnifiedResourceContextForModel) rendered
resource display names through unifiedresources.ResourcePolicyLabel, which redacts
genuinely-sensitive names UNCONDITIONALLY — ignoring the dial and even local-vs-cloud.

Symptom (reported live): on a cloud model the Assistant surfaced "redacted by
policy" and tried to run pulse_query with it as a search term, because sensitive
resources appeared redacted in the inventory regardless of the dial.

Fix: unifiedResourcePolicyContext now carries the dial + a known-local flag, and a
new resourceLabel() renders names per the dial — known-local (Ollama) always real;
cloud real only at "full" and only for resources NOT routed local-only (the same
hard floor as the sanitizer); unknown/empty destination fails closed to the
governed label (preserves the safe default for the no-destination context path).
All 15 ResourcePolicyLabel call sites in the inventory builder route through it.
This also fixes a latent inconsistency where local (Ollama) models over-redacted
inventory names despite "local is always full".

Proof: TestUnifiedResourcePolicyContext_ResourceLabelDialAware (local real;
cloud-full sensitive real, local-only floored; cloud-redacted both governed). The
existing AI-safe-summary inventory tests (no-destination path) stay green via the
fail-closed unknown-destination branch. Governance: ai-runtime contract delta —
the seam is now THREE dial-aware paths, inventory builder included. Full
internal/ai/... suite green (23 packages).
2026-06-08 18:16:50 +01:00
rcourtman fe46632e26 Stop injecting the "redacted by policy" placeholder into model directives (increment 4)
The resource-context handoff directives in internal/ai/chat/service.go and
internal/ai/chat/plain_text_resource_context.go named the literal redaction
placeholder ("Do not copy 'redacted by policy' into any tool argument", "labels
may be redacted by policy"). Naming the phrase in the prompt made the model echo
it back to the user as if it were the resource name — the confusing leakage
flagged after the sensitivity recalibration.

Reword the directives neutrally ("a withheld or placeholder label", "some labels
may be withheld") and add an explicit instruction not to repeat a withheld
placeholder back as the resource identity. The current_resource handle remains
the authoritative target, and the tool resolver still accepts the placeholder as
a defensive alias, so behavior is unchanged — only the prompt wording.

Proof: the plain-text resource-context test now pins that the directive does NOT
inject the redaction placeholder (previously it asserted the opposite, which was
pinning the leakage). Governance: substantive ai-runtime contract delta adding a
redaction-placeholder-hygiene rule for Pulse-authored model-bound directives.
Full internal/ai/... suite green.
2026-06-08 17:16:53 +01:00
rcourtman 3e3c601317 Make the redaction seam obey the cloud_context_privacy dial (increment 2)
The dial now drives model-bound redaction directly, replacing the legacy
ShareOperationalContextWithCloud read. internal/ai/chat/service.go resolves
cloudPrivacyLevel once per turn (failing closed to "redacted" when no config
snapshot is present) and threads it into the prefetch and the model boundary:

- full: the model-bound resource-policy sanitizer is invoked with
  modelboundary.RedactLocalOnlyResourcesOnly(), so real identifiers (hostname,
  IP, alias, name) for ordinary (Internal) and Sensitive (local-first) resources
  reach the cloud model — the core "answer with real detail" win. Resources the
  policy engine routes local-only (Restricted) stay redacted as a HARD FLOOR a
  blanket dial must never override, so default-full never ships a must-not-leave
  resource to a cloud vendor. Prompt-secret sanitation (credentials) always runs.
- redacted: every policied resource's identifiers are redacted as before, and the
  prefetch surfaces the PII-free operational context for governed resources.
- local_only: the prefetch injects NO proactive infrastructure context to the
  cloud turn (only a transparency directive pointing at the setting / a local
  model), and the sanitizer still redacts identifiers as a backstop.

CloudContextPolicy now carries the dial Level (failing closed to redacted for
empty/unknown) instead of a ShareOperationalContext bool; sharesCloudOperationalContext
covers full+redacted, suppressesCloudContext covers local_only. The obsolete
"Share operational context with cloud models" transparency string is replaced by
a local_only directive referencing "Cloud model privacy".

modelboundary gains RedactLocalOnlyResourcesOnly() + localOnlyRoutedResources();
the resource-redaction pass narrows to the local-only floor at full while
prompt-secret sanitation is unconditional. Local (Ollama) models never reach the
sanitizer.

Governance: substantive ai-runtime contract delta describing the dial-driven seam
and the local-only floor. Proofs: modelboundary sanitizer tests (full keeps the
floor + redacts secrets; default redacts all identifiers), CloudContextPolicy
level semantics + prefetch full/redacted/local_only behavior, and the handoff
relationship test pinned to redacted. Full internal/ai/..., unifiedresources,
and agentcontext suites green.
2026-06-08 17:08:14 +01:00
rcourtman 7cb45489e8 Add cloud_context_privacy dial (privacy redesign increment 1)
Introduce the single privacy dial that will govern what infrastructure context
cloud models may see, replacing the binary share_operational_context_with_cloud
toggle as the canonical operator control. This increment adds and surfaces the
setting; it does not change the redaction seam (that is increment 2).

Config (internal/config/ai.go): add AIConfig.CloudContextPrivacy with the
full|redacted|local_only levels, default "full", plus NormalizeCloudContextPrivacy
and the nil-safe GetCloudContextPrivacy getter. NewDefaultAIConfig defaults a fresh
self-hosted install to "full" so the Assistant answers with real resource detail
out of the box. The legacy ShareOperationalContextWithCloud boolean is retained as
the field the redaction seam still reads until it is wired into the dial directly.

Migration (internal/config/persistence.go): LoadAIConfig derives the dial from the
legacy toggle for pre-dial configs (legacy on -> full, off/absent -> redacted) and
persists it, leaving the legacy boolean untouched so existing installs keep their
current cloud behavior byte-for-byte. Fresh installs (no config file) default to full.

API (internal/api/ai_handlers.go): round-trip cloud_context_privacy through
/api/settings/ai field-by-field like discovery_enabled. The response always
serializes GetCloudContextPrivacy() (no omitempty) so the UI binds a 3-option
control to the concrete value; the update request carries an optional *string
validated against NormalizeCloudContextPrivacy (unknown values -> 400). When the
dial is provided it supersedes and re-syncs the legacy boolean (full -> true,
redacted/local_only -> false) so the existing seam honors the dial's full/redacted
axis without new redaction code paths.

Frontend: replace the binary "Share operational context with cloud models" toggle
with a "Cloud model privacy" 3-option FormSelect in AIRuntimeControlsSection.tsx,
bound to state.form.cloudContextPrivacy and the cloud_context_privacy payload via
useAISettingsState. CloudContextPrivacy type + payload fields in types/ai.ts;
label/help/option/summary copy in aiSettingsPresentation.ts.

Governance (ai-runtime + frontend-primitives substantive deltas; dependent
api-contracts, agent-lifecycle, storage-recovery notes): the contracts now name the
dial as canonical with the legacy boolean as the synced/migrated seam field.
Proofs: ai_config_test.go (getter/normalize/default), persistence_ai_test.go
(migration cases), ai_handlers_test.go (round-trip + legacy sync + 400),
contract_test.go JSON snapshots, settingsArchitecture + aiSettingsPresentation tests.
Live-verified in the preview drawer: dial renders with all three levels, the
migrated value (redacted) is selected, the summary updates reactively, and an
end-to-end UI save round-trips full (legacy sync true) then restores redacted.
2026-06-08 16:43:50 +01:00
rcourtman accebfb305 Infer resource_type from a canonical handle in pulse_query get
In the trace for 'hows esphome', the model called get with the canonical handle
'system-container-599a2e3...' as resource_id and no resource_type, and it failed
twice with 'resource_type is required' before recovering with 'get 102'. The user
sees those failed tool calls in the chat.

A canonical handle already encodes the type (unifiedresources/ids.go builds ids
as '<type>-<hash>'), so executeGetResource now infers resource_type from the
handle when it's omitted, via resourceTypeFromCanonicalID (the trailing hex hash
segment is unambiguous since no type word is all-hex). A bare numeric VMID still
requires an explicit type. Test TestResourceTypeFromCanonicalID covers it; full
internal/ai/tools green.
2026-06-08 15:49:09 +01:00
rcourtman 9573c6f625 Stop withholding tools from short resource lookups
User asked 'hows esphome' and the Assistant replied it had 'no infrastructure
context or diagnostic tools available' and asked them to run docker ps and say
where esphome lives — for a container (CT 102 esphome on delly) Pulse already
inventories.

Root cause: assistantPromptLooksConversational treated any prompt with <= 3 words
as chit-chat, routing it to the text-only scope that offers ZERO tools. So every
natural short lookup ('hows esphome', 'check frigate', 'grafana cpu') had its
tools withheld and the model genuinely couldn't query. This is the prompt-keyword
router anti-pattern the contract forbids.

Remove the word-count rule: only explicit greeting/meta prompts (hi, thanks, who
are you) are conversational; everything else is offered tools and the model
decides whether to use them. Verified live: 'hows esphome' now returns its real
status (Online, CPU 4.2%, mem 8.4%, no alerts) from Pulse data.

Regression test TestToolsForAssistantTurn_ShortResourceLookupGetsTools asserts
short lookups get tools while greetings stay text-only. Full internal/ai/chat green.
2026-06-08 15:36:58 +01:00
rcourtman ddc480ff3c Recalibrate resource sensitivity: ordinary workloads aren't secret
User on a cloud model saw 'redacted by policy' everywhere. Root cause: the
default classification (classifyResourceSensitivity) treated every VM, container,
pod, k8s workload, and docker service as 'Sensitive', which redacts their
hostname/IP/alias/path for cloud models. For Pulse's homelab/SMB audience that
crippled the cloud Assistant — a workload named 'grafana' isn't a secret, and its
private LAN IP isn't either.

Recalibrate: compute workloads classify as 'Internal' (cloud-summary, no
redaction) so cloud models can see their names/IPs. Escalation to
Sensitive/Restricted is by tag (database, backup, customer-data, secret, ...) or
by genuinely sensitive TYPE: storage/data-at-rest (storage, PBS, Ceph,
physical-disk, network-share, network, k8s PV/PVC/StorageClass), configuration
(docker-config, k8s-configmap), and security (k8s RBAC, secrets, PMG). Secrets
and PMG stay Restricted; the tag-based escalation is unchanged.

Tests: new TestRefreshPolicyMetadata_PlainComputeWorkloadsAreInternalNotRedacted
+ TestComputeWorkloadPolicyIsInternalUnlessEscalated lock it in. ~13 AI-subsystem
redaction tests that assumed plain compute = Sensitive updated to tag their
fixtures so they still exercise redaction on a genuinely-sensitive resource (no
assertions weakened). Contract: unified-resources Extension Points documents the
recalibrated classification. internal/ai/... + internal/unifiedresources/... green.
2026-06-08 15:21:51 +01:00
rcourtman 32523c8c94 Make the no-narrative fallback summary clean, not a JSON/call-id dump
When the model runs tools but returns no final narrative, Pulse synthesized a
fallback summary. It leaked the provider call ids as the 'tool names'
(normalizeToolUseID returns call_27f0f389… unchanged because the hex suffix
isn't all digits) and appended a raw JSON tool-output snippet — so the chat
showed 'I completed 4 successful check(s) using call_27f0f389…, … automatic
summary. Latest successful result snippet: {"systems":[]…}'. Far more
un-OpenCode than anything in the transcript.

buildAutomaticFallbackSummary now resolves each tool result's provider call id to
the real tool name from the assistant tool call (pulse_ prefix stripped), drops
opaque call_/toolu_/fc_ ids entirely, removes the raw-output snippet, and reads
as a clean operator message: 'I ran N checks (query, metrics) but the model
didn't return a written summary this time. Ask me again and I'll pull the
results together.'

New regression test reproduces the real OpenRouter shape (call_ ids on results,
real names on the tool calls) and asserts no call_ ids, real tool names, no raw
JSON. Full internal/ai/chat suite green.
2026-06-08 14:47:59 +01:00
rcourtman f68a75f012 Move Assistant live status to the footer, keep the transcript clean (OpenCode model)
User: the chat populates too much during a turn vs OpenCode. Read OpenCode's TUI
(packages/tui/src/routes/session/index.tsx): live 'working' state lives in ONE
pinned footer line (spinner + interrupt), and the scrolling transcript holds only
durable artifacts (user text, reasoning, tool calls, the answer). It never
narrates 'Preparing context / Reading inventory / Counting' into the timeline.

Pulse was doing both — workflow status rendered as transcript rows AND a header
chip AND (briefly) in the activity dock. Mirror OpenCode:
- MessageItem no longer renders workflow status in the transcript: the per-event
  role=status row (shouldRenderWorkflowStatusEvent) and the early-phase header
  chip (shouldShowHeaderWorkflowStatus) are both hard-false.
- The activity dock is now the single live indicator and persists for the whole
  turn: gate it on the streaming assistant message (assistantTurnActive) instead
  of chat.isLoading(), which flips false at visible-turn-complete and made the
  dock flash its status for a frame then vanish.

Result (verified live): transcript = user msg -> model route -> compact tool
rows -> answer; live status (spinner + 'Model is reasoning...' + route + Stop)
stays pinned in the footer while working, gone when done.

9 MessageItem tests that asserted the old transcript-row behavior rewritten to
assert footer-owned suppression; pacing/retry coverage stays in activeTurnStatus.
840 chat tests green (1 pre-existing ModelSelector failure, unrelated). Contract:
canonical footer-only rule added, supersedes the per-row transcript-status rules.
2026-06-08 14:39:36 +01:00
rcourtman 332fce866e Morph the streaming answer DOM instead of replacing innerHTML each tick
Final piece of the streaming-jank work. AssistantMarkdownBlock rendered
renderMarkdown(text) into innerHTML on every paced reveal, which rebuilds the
entire prose subtree — so a multi-paragraph or list/table answer flickers and
reflows every earlier line as it streams. Measured live: a 5-item numbered list
produced ~232 DOM mutations / 106 list-item removals over one turn.

Add markdownMorph.ts: reconcile old and new trees in place — identical nodes
untouched, same-tag nodes morphed (recursing into children so a growing
<ol>/<table> keeps its earlier <li>/<tr>), growing tail block updates its text
node rather than rebuilding. AssistantMarkdownBlock now feeds the sanitized HTML
to it via a ref+effect.

Same list answer after: ~22 mutations / 1 list-item removal — a ~10x drop. The
earlier lines stay put; only new items append and the tail updates in place.

Security unchanged: renderMarkdown (DOMPurify) remains the sole sanitization
gate; the morph only reconciles already-sanitized nodes. 9 morph unit tests +
286 MessageItem/AIChat tests green; tsc clean; render verified correct live.
2026-06-08 12:00:28 +01:00
rcourtman c3cd811b98 Render Assistant stream-event rows with <Index> so they stop re-mounting
Follow-up to the message-level reconcile fix. One level down, the per-message
stream-event/tool-row list in MessageItem rendered groupStreamEventsForDisplay()
through a reference-keyed <For>. That memo remaps blocks to fresh objects every
tick, so the streaming answer block (and completed tool rows) re-mounted — and
re-parsed markdown — on every content delta.

StreamDisplayEvent has no id, but the grouped list is strictly append-ordered
(the grouper only pushes new blocks or mutates the open content/thinking block in
place; never inserts mid-list or reorders), so positional keying is correct.
Switch the inner list to <Index>: each row keeps its DOM node across event-object
rebuilds at a stable position and updates in place.

Verified: <Index> reuses DOM nodes per position (probe); regression test proven
to fail on the original <For> (row re-created) and pass with <Index>; 286
MessageItem+AIChat tests green; tsc clean (it flagged every evt -> evt() spot).
2026-06-08 11:30:07 +01:00
rcourtman 7f3a0e3dd9 Stop the Assistant transcript re-mounting on every stream event
The chat felt janky during a turn — status rows popping in and out, the answer
flashing, the transcript jumping up and down — unlike OpenCode's stable timeline.

Root cause (measured with a live DOM mutation observer): useChat rebuilds its
message array immutably on every stream event, spreading a brand-new message
object each time. ChatMessages rendered that array through <For>, which keys by
object reference, so the whole MessageItem was torn down and recreated on every
content chunk / workflow-status change / tool update — dozens of re-mounts per
turn (observer showed the assistant message block DEL+ADD ~30x over 17s).

Fix: reconcile the incoming array into a keyed solid-js/store mirror in
ChatMessages so each message keeps a stable identity across updates. MessageItem
already reads every field through accessors, so once it stops re-mounting only
the genuinely changed text/rows update in place. After the fix the observer
showed the assistant block mount ONCE per turn (ADD:1, DEL:0).

Contained to ChatMessages.tsx — no changes to useChat's 18 update sites and no
new dependency. Regression test proven to fail on the old reference-keyed <For>.

Residual: the per-message stream-event/tool-row list still re-mounts on tool
turns (groupStreamEventsForDisplay remaps to new objects) — a separate, narrower
follow-up, noted in the ai-runtime contract.
2026-06-08 11:11:05 +01:00
rcourtman 3465a3bbf4 Give Assistant the current time so it stops deflecting time questions
Asked 'what's the time' in autonomous mode, Pulse Assistant deflected
('I don't have access to a real-time clock... tell me a target host and I can
run `date`') while OpenCode just ran date and answered. Root cause: the
Assistant's per-turn system prompt carried no clock, and the heavy target_host
framing pushed the model to demand a host for any command.

Inject the current wall-clock time (Pulse server clock) into the per-turn
prompt in AgenticLoop.getSystemPrompt. getSystemPrompt is re-evaluated each
turn, so the timestamp stays fresh; the base prompt is frozen at service start
and must not carry it. The time is PII-free and safe on cloud-routed turns.

The Assistant now answers time/date questions directly with no command and no
target host.
2026-06-08 10:39:18 +01:00
rcourtman 3f76da7932 Surface cloud operational-context sharing opt-in in AI settings
Wire AIConfig.ShareOperationalContextWithCloud through /api/settings/ai so the
existing chat-path opt-in (commit 32d597267) is operator-reachable, not
config-file-only.

Backend (internal/api/ai_handlers.go): add share_operational_context_with_cloud
to the AI settings response (always serialized so a toggle can bind to the
concrete value) and to the update request as an optional *bool, applied
field-by-field exactly like discovery_enabled (omitted = persisted opt-in
unchanged).

Frontend: add a 'Share operational context with cloud models' toggle to the
Assistant runtime controls, bound to the canonical useAISettingsState form and
the api/ai.ts AISettings/AISettingsUpdateRequest payload. Help/summary copy
(PII-free scope, hostnames/IPs/aliases stay redacted, default off, local Ollama
always gets full context) lives in aiSettingsPresentation.ts.

Governance: substantive ai-runtime + frontend-primitives deltas plus
dependent-contract notes (api-contracts, agent-lifecycle, storage-recovery);
path-policy proofs in ai_handlers_test.go (round-trip), settingsArchitecture
and aiSettingsPresentation tests. JSON snapshot contracts updated for the new
always-serialized field.
2026-06-08 10:05:37 +01:00
rcourtman 32d5972673 Share PII-free operational context with cloud Assistant when opted in
Governed resources (every sensitive guest) were redacted to a terse
summary on cloud-routed Assistant turns, so the Assistant went blind on
cloud models -- generic non-answers for the majority of users who run
cloud providers.

Add AIConfig.ShareOperationalContextWithCloud (default false). When the
operator opts in and the turn routes to an external provider, the chat
prefetch path injects servicediscovery.FormatCloudSafeContext (service
identity, access command, config/data/log paths, ports -- PII-free) in
place of the terse governed redaction, and the model-bound resource
sanitizer allow-lists those exact spans so they survive the provider
boundary. Hostname/IP/alias/platform-id stay redacted regardless of the
opt-in.

When sharing is off on a cloud turn, the prefetch path instructs the
Assistant to disclose the redaction and point at the setting instead of
silently degrading the answer. Local (Ollama) routing is unaffected and
always receives full context.

Proof: internal/ai/chat/context_prefetch_cloud_context_test.go (opt-in =>
access path present, no hostname/IP; opt-out => governed redaction +
transparency; model-bound sanitizer strips raw PII while the allow-listed
cloud-safe span survives) and internal/config/ai_config_test.go.
ai-runtime contract updated for the new opt-in behavior.
2026-06-08 09:38:22 +01:00
rcourtman 091ffc2f3d Keep Assistant chat model routes explicit
Fail unusable selected chat routes instead of substituting a same-provider default so retries and errors stay tied to the chosen model.
2026-06-08 07:21:38 +01:00
rcourtman d89257f9d8 Add Assistant tool chain fixture
Add a paced local Assistant fixture that exercises consecutive tool start, progress, completion, and replacement states without opening a provider request.
2026-06-08 07:13:37 +01:00
rcourtman 7cf9b81210 Show Assistant prompt send status
Seed Assistant turns with a local prompt-send status, promote it when the chat stream opens, and keep backend workflow activity as the durable stream evidence.
2026-06-08 06:53:51 +01:00
rcourtman 13b2c33dfd Align Assistant route wait status
Use neutral assistant wait status while keeping selected model route metadata visible. Make transient retry copy explicit that Pulse retries the selected route rather than switching providers.
2026-06-08 06:37:47 +01:00
rcourtman 9992ecf05a Show Assistant tool starts as live running activity 2026-06-08 04:15:15 +01:00
rcourtman 10fa9eca3a Mark Assistant provider retries as live activity 2026-06-08 04:00:30 +01:00
rcourtman 50295914c1 Remove hidden discovery provider fallback 2026-06-08 03:48:33 +01:00
rcourtman d9e9b952e7 Show queued Assistant route identity 2026-06-08 03:33:11 +01:00
rcourtman 6b64ace474 Contain Assistant model catalog failures 2026-06-08 03:20:44 +01:00
rcourtman 2f1ec1fa61 Show Assistant last turn summary 2026-06-08 03:13:19 +01:00
rcourtman 4a1e36af83 Accept provider/model Assistant routes 2026-06-08 02:15:21 +01:00
rcourtman c95c7ff612 Keep Assistant available around feedback prompts 2026-06-08 01:11:40 +01:00
rcourtman 02386309d7 Speed up Assistant stream status feedback 2026-06-08 00:56:55 +01:00
rcourtman 17ef976140 Expose Assistant fixture commands 2026-06-08 00:24:25 +01:00
rcourtman a06d7686b6 Show Assistant provider retries immediately 2026-06-07 23:56:08 +01:00
rcourtman 98ee858f7f Keep completed Assistant tool output inspectable 2026-06-07 23:41:32 +01:00
rcourtman a4c5a85bef Keep Assistant route labels provider-explicit 2026-06-07 23:25:26 +01:00
rcourtman 59a91042d9 Keep Assistant retry routes session-faithful 2026-06-07 23:11:19 +01:00
rcourtman 621391a125 Pace burst Assistant workflow statuses 2026-06-07 22:48:43 +01:00
rcourtman 778eaac06e Clear terminal Assistant workflow status rows 2026-06-07 22:27:46 +01:00
rcourtman 11d442c083 Ignore retired Assistant provider fallback events 2026-06-07 21:59:44 +01:00
rcourtman ddd9aaddba Keep fast Assistant tool rows visibly running 2026-06-07 21:48:12 +01:00
rcourtman a8833dca2c Add provider settings action to Assistant model picker 2026-06-07 21:37:29 +01:00
rcourtman 691decb845 Align Assistant live workflow status 2026-06-07 21:19:55 +01:00
rcourtman db6694774f Align Assistant slash command empty state 2026-06-07 20:57:34 +01:00
rcourtman 33701fe274 Preview safe Assistant tool output snippets 2026-06-07 20:37:40 +01:00
rcourtman 4b826d5403 Preserve Assistant workflow activity on completed turns 2026-06-07 20:25:05 +01:00
rcourtman fa50307212 Clarify Assistant route recovery actions 2026-06-07 20:08:29 +01:00
rcourtman 2e486fbbf9 Align Assistant command availability with toolbar state 2026-06-07 19:57:56 +01:00
rcourtman 23f520f7e7 Make Assistant idle liveness route-aware 2026-06-07 19:43:43 +01:00
rcourtman 586f80c837 Compact live Assistant tool activity 2026-06-07 19:21:31 +01:00
rcourtman 4e270cfd50 Improve Assistant model command search 2026-06-07 19:08:28 +01:00
rcourtman edabb6c059 Hide successful Assistant tool output previews 2026-06-07 18:50:28 +01:00
rcourtman a4a14e4911 Require explicit Assistant route recovery 2026-06-07 18:35:25 +01:00