# feat\(insights\): add optional AI Insights usage card \(\#339\) · lucenx/codexbar-plasma

[View on GitCafe](https://git.cafe/lucenx/codexbar-plasma/commit/04c3a1745c0109c78704bca6a348ef8df2426e90)

Repository: [lucenx/codexbar-plasma](https://git.cafe/lucenx/codexbar-plasma)

Visibility: public

Requested revision: 04c3a1745c0109c78704bca6a348ef8df2426e90

Requested commit: 04c3a1745c0109c78704bca6a348ef8df2426e90

Commit: 04c3a1745c0109c78704bca6a348ef8df2426e90

Tree: 17df4f4e36afe30d85d3e3dc0f29c783f8ad8a2e

Author: Simone

Committer: GitHub

## Message

```
feat(insights): add optional AI Insights usage card (#339)

* feat(insights): add optional AI Insights usage card

Short AI-generated explanation of current usage at the end of Overview
(or the provider view without an Overview tab), off by default. While
off, the widget builds no AI data, starts no AI process, and shows no
card. Once enabled, generation is manual or on a 6/12/24-hour interval;
usage refreshes, opening the popup, and language changes never start a
request, and a plasmashell restart waits at least 30 minutes.

Only an allowlisted numeric snapshot is sent (quota percentages, window
lengths, reset times, CLI pace forecasts, incident severity, and
week-over-week spend and token totals with 14 complete days of history;
no account names, emails, projects, paths, or prompts). Requests go
through a packaged Python helper: Ollama local or https-only remote,
OpenRouter with data_collection deny and optional Zero Data Retention
routing, OpenAI with store:false. Redirects are refused. API keys are
prompted with kdialog and stored through Secret Service inside the
helper; they never reach QML, a command line, or widget settings.

Insights are written in the language the interface actually displays,
read from each catalog's BCP 47 marker entry, and the language is part
of the saved insight's identity. QML-side endpoint input is bounded to
the helper's own 2048-character limit so oversized values fail as
invalid input rather than an unexecutable command line.

Docs: new docs/ai-insights.md contract plus usage, development,
translations, and docs-index guides, README requirements, SECURITY
scope, AGENTS.md secret rule, CHANGELOG Unreleased, and all five
translation catalogs.

* fix(insights): address review feedback and repair CI failure

Treat truncated and malformed answers as permanent failures, so automatic
generation waits for the next scheduled interval instead of retrying every
30 minutes with billed requests.

Measure the snapshot run-out forecast from the pace observation time, like
the panel countdown, instead of copying the raw ETA next to a current
reset time.

Keep the single-provider smoke insight to Codex, driven by the snapshot
provider states, and fix the double-escaped skip regex in the controller
test.

Rename the controller's enabled property to insightsEnabled: it shadowed
Item.enabled, and Qt 6.11 warns about the override, which failed the
zero-warning assertion in CI.

* fix(insights): surface invalid commands and correct retry docs

An unbuildable helper command (for example an oversized Ollama endpoint)
no longer fails silently: the controller records invalid_input, so the
card shows it and automatic generation waits out the interval, and the
settings Test connection button reports it instead of clearing the
status line.

Extract AiInsights.safeReason() shared by the generation and models
reply parsers.

Docs: malformed answers wait for the next scheduled generation rather
than never being retried; language changes do not make a request due
by themselves; record the Ollama temperature 0.2 field; document that
the Retry-After wait is in-memory and restarts reset it to the
persisted 30-minute attempt guard.

Tests: controller invalid-command scenario plus direct safeReason
cases; the new scenario fails without the controller change.

* fix(insights): respect the interval after failures and persist rate limits

Count every attempt, including a failed or timed-out one, against the
automatic interval. A timeout or network error previously allowed another
automatic request 30 minutes later, up to 12 possibly billed requests per
6-hour interval. The unused 30-minute transient delay is removed.

Persist a provider's Retry-After deadline with the context it applies to
(internal aiInsightsRateLimit key), so a plasmashell restart no longer
lifts it for automatic or explicit requests. A blocked explicit request now
reports the rate limit instead of doing nothing. Another context ignores the
deadline, a deadline beyond 24 hours is void, and a success clears it.
This supersedes the documented in-memory-only Retry-After limitation.

Make Test connection report whether the configured model is in the listed
models (untagged Ollama names match :latest) instead of always claiming
success; it still sends no paid inference, and says that OpenRouter's Zero
Data Retention routing is checked only on generation.

Bypass environment proxies for a loopback Ollama endpoint, so statistics
promised to stay on this computer never reach an http_proxy.

Documentation: docs/ai-insights.md, docs/usage.md, and docs/development.md
describe the schedule, rate-limit persistence, Test connection, and proxy
behavior. Catalogs gain the three new settings messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgNKDTEVv5B4izQQK3jxw3

* fix(insights): polish the card and settings, mark the feature beta

- Mark AI Insights as beta in the settings sidebar and card heading. Italian
  now uses "Analisi IA", which no longer breaks mid-word in the sidebar.
- Give the generate button a wand icon so it is not confused with the
  usage refresh button, show only the model part of vendor/model ids, and
  keep wrapped highlights aligned after their bullet.
- Dim the insight while a new one is generated and let the reply settle in
  with a short opacity transition; opening the popup never animates.
- Cap the API key row at the form width so switching providers no longer
  re-centers the settings page, and keep each provider's model while the
  page is open.
- Shorten the privacy note, rename "Generate:" to "Frequency:" and the
  saved-insight button to "Clear".
- Ask for a one- or two-sentence summary, highlights that add new facts,
  the language's decimal separator, and no list of unavailable providers.
- Make the controller retirement tests independent of whether
  Plasma5Support stops a retired process before the helper logs it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): align the generate button with the Overview chevrons

Center the card's generate icon on the chevrons of the Overview provider
rows instead of leaving it a few pixels to their left.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): cap the height of the model list

OpenRouter lists hundreds of models, and the desktop style's combo box
menu grew to cover the whole settings window. Cap it at 16 grid units so
it scrolls; typing still completes a model name. The settings smoke
scenario now loads a long synthetic list, opens the menu, and fails
without the cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): give the card the Overview row structure

- Add an identity tile with a hint bulb, matching the provider rows'
  tile, so the title lines up with the provider names above it.
- Show "Beta" as a quiet badge beside "AI Insights" instead of in the
  card title; the settings sidebar keeps "AI Insights (Beta)".
- Use small accent dots for highlights, centered on their first line.

No new motion: the regenerate dim-and-settle transition is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): make the card quicker to read

- Show the summary first and put highlights behind the same flat
  "Show details" toggle the cost section uses.
- Give the insight's age as relative time, like the provider rows, and
  mark an out-of-date insight beside its age instead of in an extra line.
- Offer a labeled "Generate insight" button while no insight exists; the
  header wand appears once there is one to refresh.
- Ask the model to write increases above 300% as multiples.
- Drop the settings section heading that repeated the page title.

The card smoke scenarios now start generation through the first-use
button and check that highlights stay collapsed until expanded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): leave room for reasoning models

Reasoning models, such as deepseek-v4.1-flash on OpenRouter, spend hidden
tokens before the JSON answer, and those count against the output bound.
At 1000 tokens their answers were often cut off.

- Raise the output bound to 4000 tokens for Ollama, OpenRouter, and
  OpenAI. Billing covers only the tokens actually produced.
- Extend the timeout chain to 150 s (HTTP), 180 s (shell), and 185 s
  (QML deadline), and test that it stays ordered.
- Explain a truncated answer: the model ran out of room, often while
  reasoning, so try a model without reasoning.

OpenRouter's reasoning parameter is not sent: with require_parameters it
would exclude every model that does not reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): make local Ollama models usable

Tested against Ollama 0.34.3 on an RTX 5070 with a synthetic snapshot:

- qwen3:4b thought for the whole 4000-token bound (about 26 s) and never
  answered. With "think": false it returns valid JSON in about 2 s.
- Small models answered in English despite the system instruction.
  Repeating the output language after the usage data makes qwen3:4b
  write Italian; qwen2.5:1.5b still does not, and the usage guide now
  recommends models of about 4B parameters or more.
- Ollama kept each model in GPU memory for five minutes after a
  generation. Send "keep_alive": 0 so a rarely generated insight
  releases it at once; "ollama ps" is empty right after the reply.

"think": false was accepted by both a thinking and a non-thinking model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): turn off paid reasoning and attribute OpenRouter requests

Verified against the OpenRouter docs and real requests:

- Reasoning tokens are billed as output. For models whose public
  /models/{id}/endpoints metadata lists "reasoning", send
  reasoning.effort "none": deepseek-v4.1-flash dropped from about 1000
  to about 100 output tokens (about $0.0005 to $0.00013 per insight).
  The parameter is not sent to other models, because with
  require_parameters it left mistral-nemo without a route. A failed
  metadata lookup keeps the previous request.
- Send HTTP-Referer, which OpenRouter requires for app attribution, and
  X-OpenRouter-Title, so logs show "CodexBar Plasma" instead of Unknown.
- Hide ":batch" variants from the model list: they serve only the
  asynchronous Batch API.
- Share the popup's relative-age helper (elapsedText) instead of calling
  the session helper, and show "Out of date" without "Just now" for an
  insight that is stale right after generation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): turn off reasoning only on a routable endpoint

require_parameters routes only to an endpoint that accepts every sent
parameter, so send reasoning.effort "none" only when one endpoint lists
both reasoning and structured_outputs, not when they are split across
endpoints. Privacy routing (data collection, Zero Data Retention) can
still exclude that endpoint, and the metadata cannot tell, so a request
that finds no route is retried once without the parameter. An unrouted
request reached no provider and was not billed; other failures are not
retried.

Addresses the CodeRabbit review on 5580360.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(insights): retry rejected reasoning and bound the whole generation

OpenRouter models whose reasoning is mandatory reject effort "none" with
400, which mapped to `request` and was never retried, so those models
always failed. A 400/422 reached no model and was not billed; it is now
retried once without the parameter, like an unrouted request.

The 150 s HTTP timeout bounded only one call, while a wallet unlock, the
model lookup, and the retry could together outlast the 180 s shell bound,
which then killed a possibly billed request and surfaced it as a format
error. Generation now shares a 170 s budget, never starts a request with
less than 30 s left, and the controller maps GNU timeout's exit codes 124
and 137 to `timeout`.

Documentation: docs/ai-insights.md describes the retry reasons and the
budget; docs/development.md corrects the controller deadline to 185 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dfoVaJVXCHdJHtTEN3ZUn

* fix(insights): keep Ollama prompts whole and simplify the model's task

Ollama defaults to a 4096-token context on most computers and silently
drops the start of a longer prompt, where the instructions are. With
eight providers the prompt alone reaches about 3500 tokens, leaving no
room for the 4000-token output bound. `num_ctx` is now sized from the
prompt plus the output bound, and temperature is 0, as Ollama's
structured-output guide recommends.

The instructions no longer ask the model to divide: an increase above
300% carries a precomputed `changeMultiple` in the snapshot and its
signal. Length targets are in words instead of characters, and the
response schema is repeated in the prompt, as Ollama recommends.

Documentation: docs/ai-insights.md describes `changeMultiple`, the word
targets, the schema in the prompt, and the Ollama options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dfoVaJVXCHdJHtTEN3ZUn

* fix(insights): send only current providers and one form of each change

Measured on local and cloud models, the snapshot still invited two
mistakes. Models mentioned providers without current data despite the
rule to leave them out, and with both `changePercent` and
`changeMultiple` present, qwen3:4b kept the percentage while deepseek
converted other percentages itself and got them wrong ("+108%" became
"almost tripled").

The snapshot now lists only providers with a current measurement, which
it already required before any request, so the prompt rule about stale
providers is gone. An increase above 300% carries `changeMultiple`
instead of `changePercent`, and the instructions forbid turning a
percentage into a multiple or a fraction.

Documentation: docs/ai-insights.md describes the current-only snapshot
and the single change form; docs/usage.md updates what is sent and notes
that 4B models can drift into English with many providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dfoVaJVXCHdJHtTEN3ZUn

---------

Co-authored-by: Claude <noreply@anthropic.com>
```

## Parents

- [612b981213825f3b35ebcc004dec9bb9ac3a93c5](https://git.cafe/lucenx/codexbar-plasma/commit/612b981213825f3b35ebcc004dec9bb9ac3a93c5?format=markdown)

[Source at this commit](https://git.cafe/lucenx/codexbar-plasma/tree/04c3a1745c0109c78704bca6a348ef8df2426e90?format=markdown)
