aboutsummaryrefslogtreecommitdiffstats
path: root/CHANGELOG.md
blob: 2a503a7161b7593e0fd252bc910bdcdb8f136197 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- External OpenAI-compatible providers. The local llama.cpp router stays the
  default; adding a `[providers.*]` table brings cloud models into the picker
  as `provider:model`. The generated config ships the provider block commented
  out, so a fresh install never reaches an endpoint nobody configured.
- Three ways to supply a provider API key: `pass:name` reads from the password
  store, `env:VAR` reads from the environment, and a literal `sk-...` string
  is used as-is. `pass:` is preferred because it keeps secrets out of
  `config.toml`.
- Lazy key resolution. The password store or environment is only consulted on
  the first request to a provider, so a local-only session never triggers a
  pinentry.
- Model filtering per provider. The `filter` key keeps only models whose id
  contains one of the configured substrings, case-insensitively. Providers
  list hundreds of models, and without a filter the picker is unusable.
- A per-model settings dialog for context size, vision support and input/output
  prices. Values are saved to `models.ini` beside `config.toml`, so provider
  defaults can be overridden per model.
- Per-conversation cost readout with projection. The top bar shows what has
  been spent so far plus what the current draft would add, based on hand-entered
  prices. This exists to make visible that a long conversation on a cloud
  provider is billed for its whole history on every turn.
- Three new columns on `messages`: `prompt_tokens`, `completion_tokens` and
  `model`. Token counts come from the stream's final usage chunk and power the
  cost readout; the model column records which model actually replied, so a
  switched conversation prices each turn correctly.
- Three more columns on `messages`: `provider`, `usage_json` and
  `reported_cost_usd`. The provider name is recorded when a reply finishes so
  history stays self-describing even if a provider is later renamed or
  removed; `usage_json` keeps every round's raw usage block from the stream
  as a JSON array (a searched turn makes one API call per search round, and
  all of them are billed, so all of them survive) for external consumers
  such as the cost dashboard's ingester; and `reported_cost_usd` stores the
  GUI's hand-entered-price estimate for the reply as a sanity check only,
  staying NULL for an unpriced model rather than claiming it cost nothing.
  `prompt_tokens` and `completion_tokens` are now the turn's totals summed
  across rounds rather than the last round's counts, so a searched turn is
  priced for every API call it made.
- Per-provider `thinking_budget` option, for endpoints (currently SiliconFlow)
  that cap chain-of-thought tokens separately from the final answer. Unset
  providers skip the key entirely so other endpoints do not receive an unknown
  parameter.
- Per-provider `replay_reasoning` option. When set and web search is enabled,
  each prior assistant turn's `reasoning_content` is replayed on the next
  request, which interleaved-thinking providers (DeepSeek, SiliconFlow's
  GLM-4.7+) require when a `tools` key is present. Off by default so other
  providers do not pay context and input tokens for thinking they ignore.
- On-demand diagnostic log. Setting `LLAMACHAT_DEBUG_LOG` to a path appends
  timestamped provider-error records (status, request body, full response) to
  that file; unset, it writes nothing, so a normal launch never grows a log.
- A dismiss (✕) button on status-line messages. They previously stayed until
  some later action replaced them; any status message can now be cleared
  without waiting.

### Fixed

- Model listing crashed when a provider's `/v1/models` endpoint returned a
  bare JSON array instead of the standard `{"data": [...]}` envelope. The
  parser now accepts both shapes.
- Cloud chat requests no longer rely on the provider's default `max_tokens`,
  which could be too small for a reasoning model. Non-local providers now send
  `max_tokens = 32768`, leaving room for both chain-of-thought reasoning and
  the answer.
- A streamed tool call is now detected even when the provider puts
  `finish_reason: "tool_calls"` on the same SSE line as the usage block (the
  `include_usage` final chunk). The parser previously returned that line as
  `usage` and dropped the finish signal, so a DeepSeek web search never
  triggered and the turn ended at the thinking.
- A searched turn now replays the model's `reasoning_content` verbatim on the
  assistant tool-call message. Interleaved-thinking models (DeepSeek V3.2+/V4,
  GLM-4.7+ on SiliconFlow) require this and stop answering — ending the turn at
  the thinking — when it is dropped.

Tool calling and reasoning output vary between providers. Web search on a
cloud model may not work as reliably as it does with a local router.

## [0.3.0] - 2026-08-01

### Changed

- `max_searches` now defaults to 2, up from 1. The empty-reply failure that
  forced the lower cap has two causes, both now understood: the model was
  never told its last search was spent, fixed below, and llama-server's
  quantized KV cache degrades tool calling, which is a server setting rather
  than something this client can control. With `f16` KV and the final-round
  note, a question needing two searches answered 4 and 5 times out of 6 on
  two runs, against 3 of 3 broken before. Single-search turns are solid, 6
  of 6. Lower it back to 1 when running a quantized KV cache, or if empty
  replies are more annoying than a second search is useful.

### Fixed

- A turn that used its last search no longer ends with an empty reply. The
  final round's tool result now states that the search tool is gone and the
  answer is due. Withdrawing the tool schema was invisible to the model,
  which asked for another search regardless: this model's chat template
  defines its own `<tool_call><function=...>` XML dialect, so the request
  came back either as literal text in the reply or buried in the thinking
  block, and in both cases the reply itself was empty. The note rides on the
  tool result because a trailing system message is rejected outright, the
  template raising "System message must be at the beginning". Measured on
  the question that first showed the bug: 3 of 5 turns broken before, 5 of
  5 answered after.

### Added

- Conversation titles are written by the model. Once a reply finishes in a
  session still carrying its placeholder title, the exchange is sent back in
  a short side request asking for a title of at most six words, and the
  answer replaces the opening words of the question the entry was created
  with. The trigger is the placeholder rather than the turn number, so a
  first reply that comes back empty does not forfeit titling for the rest of
  the session, and a one-shot window, which appends every question to the
  same entry until New is pressed, is titled from the question that actually
  got answered. It runs on its own thread and is silent about failure: an
  unreachable router or an unusable reply leaves the original title in place
  rather than raising.
  Thinking is disabled for that request through the chat template, since a
  reasoning model otherwise spends the whole token budget on its thoughts
  and returns an empty answer.

- Web search through a SearXNG instance. The model is offered a `web_search`
  tool and decides for itself when a question needs current information;
  llamachat runs the query and feeds the results back. Off by default, and
  enabled only when both `search_enabled` and `search_url` are set, so an
  upgrade never starts talking to the network on its own.
- Searches appear in the transcript as a collapsible block above the
  thinking block, listing each query and its sources, with a status line
  while a search runs. The queries are visible because a bad answer is
  usually a bad query, and showing the sources is what lets a poisoned
  result be recognised as one.
- Six configuration keys: `search_enabled`, `search_url`, `search_results`,
  `search_snippet_chars`, `search_timeout` and `max_searches`. Searches are
  capped per turn, after which the tool is withdrawn and the model must
  answer.
- Searches are stored per message in a new `searches` column, so reopening a
  conversation still shows what was looked up. They are deliberately kept
  out of the full-text index, or web text nobody wrote would compete with
  the user's own messages.
- The current date is added to the system prompt when search is on. A model
  has no clock and falls back on its training cutoff, which it then writes
  into the query itself ("latest kernel ... 2025"), poisoning the results
  before they are fetched. It is also told not to date its own queries.
- A search where every engine failed is now reported as a failure naming
  the engines, instead of as a successful search that found nothing. Rate
  limits and CAPTCHAs are the normal way a self-hosted SearXNG stops
  working, and "no results" invited a confident answer from stale training
  data.

### Security

- Search results are untrusted text entering the model's context. Only
  `title`, `url` and the snippet survive, snippets are truncated, all result
  text is escaped on display, result links are shown but never fetched
  automatically, and a reply forging the search toggle's URL scheme has it
  defused the same way the reasoning scheme already was. A poisoned snippet
  can still influence what the model says; the sources are shown so that it
  can be judged.

## [0.2.1] - 2026-07-31

### Fixed

- Reasoning could be spliced into the reply. A delta carrying both
  `reasoning_content` and an empty `content` was classified by truthiness,
  so the thinking fell through to the content branch and the tail of the
  model's reasoning was stored and shown as the answer. Deltas are now
  classified on the presence of the field, and an empty reasoning delta
  renders nothing instead of falling through.
- An unbalanced backtick reflowed the rest of a reply as code. Leaked
  thinking is dense with backticks, and an odd count left a run open to the
  end of the message. An unterminated fence is now closed before parsing,
  with a closer matching the opener's length, and a lone dangling inline
  backtick is dropped. This also covers streaming, where a fence is
  unclosed on nearly every frame.

### Added

- Context meter in the top bar showing how much of the active model's
  context window the next request will use. It estimates while typing and
  switches to the server's exact token counts after each reply, turning
  amber at 75% and red at 90%. The limit comes from `ctx-size` in
  `presets.ini`, since the router does not report it.
- System prompts stored as markdown files in `~/.config/llamachat/prompts/`.
  A global `default.md` applies to new conversations, named presets replace
  it, and any conversation can take a one-off custom prompt or none at all.
  A dialog behind the picker adds, edits and deletes them; the files stay
  editable outside the app.
- `default_prompt` in `config.toml` chooses what new conversations start
  with.
- Streaming requests now ask for usage statistics, which is what makes the
  meter exact without an extra round trip.
- A toggle for the history panel, on the `☰` button and `Ctrl+\`. The
  panel's width and hidden state are remembered between runs in
  `~/.config/llamachat/state.ini`.
- `Ctrl+N` starts a fresh conversation, `Ctrl+F` jumps to the search field.

### Fixed

- Escape while filtering history dismissed the whole window. It now backs
  out of the search field first: clear the text, then return to the input,
  and only then hide.

### Changed

- Sessions record the system prompt they were built with, so reopening a
  conversation restores it rather than applying whatever is selected now.

## [0.1.0] - 2026-07-31

First working version.

### Added

- Persistent background process with a Unix-socket control channel, so a
  window toggle costs a socket round trip instead of a process start. The
  control commands (`--toggle`, `--show`, `--hide`, `--ping`, `--quit`) do
  not import Qt and answer in well under a tenth of a second.
- Two chat modes. One-shot asks a single question with no context carried
  over; Chat keeps a resumable multi-turn conversation.
- Model picker populated at runtime from the router's `/v1/models`, with
  vision-capable models marked.
- Streaming replies rendered token by token.
- Markdown rendering for replies: headings, emphasis, lists, tables, inline
  code and tinted fenced code blocks, via `QTextDocument` with no markdown
  dependency. User input is shown literally so attached file contents are
  never reflowed.
- Collapsed reasoning. Models with a reasoning budget return their thinking
  in a separate field; it shows as a one-line summary that expands on click,
  tracked per reply.
- File attachment by drag-and-drop or file dialog. Text and code files are
  inlined into the prompt and truncated to fit the model's context with a
  warning. Images require a vision model, and dropping one on a text model
  offers to switch.
- SQLite history with FTS5 full-text search. Chat sessions reopen and
  continue with context intact; one-shot entries reopen read-only.
- Attachment provenance: original path, size, SHA-256, and a thumbnail for
  images, so a reopened conversation still shows what was sent even if the
  file has since moved.
- System tray icon for show/hide/quit.
- Configuration in `~/.config/llamachat/config.toml`, written with defaults
  on first run.
- `presets.ini` parsing for the two things the API does not report: which
  models have an mmproj file, and each model's context size.
- Hyprland Lua snippets for autostart, keybind and floating window rules.

### Fixed

- `presets.ini` opens with a bare `version = 1` before any section, which
  made `configparser` reject the whole file.
- Sessions sharing a one-second timestamp sorted unpredictably in the
  history list and in search results.
- A venv PySide6 bundles almost no Qt plugins, so `QT_QPA_PLATFORMTHEME`
  found nothing and every window fell back to Fusion, ignoring qt6ct and
  Kvantum. The system plugin directory is now used when its Qt version
  matches the bundled one exactly.
- A reply containing a markdown link to the internal reasoning scheme
  produced a working toggle, letting model output drive the interface.
  The scheme is namespaced, forged anchors are rewritten before display,
  and only `http`, `https` and `mailto` links reach the desktop opener.
- Venv discovery compared resolved interpreter paths, so a venv created
  with `--system-site-packages`, whose `bin/python3` symlinks to the system
  interpreter, was discarded as "already running".

### Security

- Markdown is parsed with the `MarkdownNoHTML` flag, so markup in a reply is
  displayed rather than interpreted.
- The control socket is created mode 0600.
- Search input is tokenised and quoted before reaching FTS5, so punctuation
  cannot be read as query syntax.