Skip to content

Windows & Linux: eight fixes from daily use, one commit each (idle CPU, the chat's picker and errors, a flaky test) - #365

Open
shakibbinkabir wants to merge 8 commits into
Louis-CFM:mainfrom
shakibbinkabir:chat-fixes
Open

shakibbinkabir wants to merge 8 commits into
Louis-CFM:mainfrom
shakibbinkabir:chat-fixes

Conversation

@shakibbinkabir

@shakibbinkabir shakibbinkabir commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Eight things I ran into using Coucou on Windows every day, one commit each. This folds in #318 and #321, which I am closing, so everything is in one place.

Take any subset. Five commits stand alone, and the other three each need only the one before them. I applied every row of the table to main separately and ran the tests on each.

# Commit What Kind Needs
1 df9b63a Auto-close test no longer races its own timers Test only —
2 157f37c A hidden island stops painting Bug (0 % CPU rule) —
3 4f82cbc Room for the chat's model list Bug in the port —
4 f6798e8 A provider's error is one sentence, and a note stays in its card Bug —
5 d92b538 A chat error's note can be closed, and closes by itself Small new behaviour 4
6 bcae11b The picker marks a model its provider refused Small new behaviour —
7 f5dd758 A refused model stays marked between launches Small new behaviour 6
8 89d2b70 The picker shows the providers that are set up Proposal, departs from the Mac —

They are ordered from "plain fix" to "your call". Commit 1 is first so that this PR's own Build check stops failing at random.

What changes on screen

Shipped views stay as they are. These are the only visible differences, so you can judge them against the no-restyle rule:

  • 3: while the chat's model picker is open, the island is 312 px tall instead of 240. Nothing inside it changes.
  • 4: a note longer than four lines ends with an ellipsis instead of filling its card.
  • 5: the note shown after a chat error gets the finished card's "OK" button.
  • 6: a model the provider refused is greyed in the list with "Not available" (a string the app already has).
  • 8: a cloud provider without a key is not offered in the picker.

Commits 1, 2 and 7 change nothing you can see.

The repo's rules

  • Secrets: the picker only asks the credential store whether a key is there, as before. No key leaves it.
  • Network: no new request anywhere. The "Not available" marks come from the answers to real chat turns; nothing is probed.
  • 0 % CPU when hidden: commit 2 is about exactly that. The only timer added is the note's single 10 s timeout, which exists only while a note is up.
  • Disk: one new file, refused-models.json next to the log, with model names only. The uninstaller removes it with the log. settings.json gets no new field.
  • Mac and iPhone: no app code is touched. Commit 1 changes one Swift test file.
  • Pill IDs: untouched.

The commits

1. Auto-close test no longer races its own timers (was #321)

"Test auto-close timing" fails now and then on PRs that do not touch the state machine: line 27 on #307, line 54 on #320 and on this PR. Two cases sleep for a fixed time and then assert that a pending timer has not fired, with 150 ms and 100 ms to spare. Both timers are now a full second out. What each case checks is unchanged, and the test takes about 1.6 s longer.

On a macos-latest runner on my fork, compiled the way scripts/test-auto-close.sh does:

Old test New test
Idle, 10 runs 0 failed 0 failed
9 busy loops on 3 cores, 40 runs 0 failed 0 failed
Process frozen 200 ms out of every 500 ms, 30 runs 21 failed (16 at line 27, 5 at line 54) 0 failed

Plain CPU load did not reproduce it. Freezing the process did, at the two lines CI fails on. The freeze is my model of a stalled runner, not something I observed on the failing runs.

2. A hidden island stops painting (was #318)

style.css pauses infinite animations only in views that are not .on. The view that is on stays on while the island is compact or hidden, so the ticker's shimmer kept repainting at 60 fps inside an island of zero height. #content now carries .live while it is visible, and the existing rule also covers #content:not(.live) *.

CPU time of coucou.exe plus its WebView2 processes over 30 s, starting 3 s after the window shrinks to the wake strip, after a Claude Code session:

Total Renderer GPU process
0.2.0 as released (two runs) 2,297 ms and 2,297 ms 1,344 ms 797 ms
With this commit 656 ms 500 ms under 15 ms

About 0.5 s per 30 s remains, and this commit does not address it. It is not the page: a CDP profile of the hidden island shows 0 ms of script, layout and style over 20 s, and the AudioContext is suspended. It sits on Chromium's thread-pool threads and I could not attribute it to app code.

3. Room for the chat's model list

On macOS the picker is a popover with 260 pt for the list. In the port it sits inside the island, in whatever the chat leaves: 23 px for rows 28 px tall. While the picker is open the island now takes all the height the panel has (PANEL_H - 8), the way the question view already grows for its options.

Before After
Island List Models fully visible (of 14)
Before 240 px 23 px 0
After 312 px 95 px 3

It does not reach the Mac's 260 pt. That needs a taller window, and PANEL_H is also the desktop Mochi's home zone and the Linux geometry, so I left it alone.

4. A provider's error is one sentence, and a note stays in its card

A chat error is shown as a note, in a card made for one short line. The cloud providers' errors carried the provider's whole message after our own sentence. Gemini's 404 for a model new accounts can no longer use adds three sentences of developer advice and a link: 341 characters over five lines, filling 100 of the card's 108 px.

  • net::error_detail keeps the provider's first sentence. Its comment already said "cut down for a message", but only plain-text bodies were cut.
  • The note is held to four lines with an ellipsis, with the whole text in its tooltip.
Before After
5. A chat error's note can be closed, and closes by itself (needs 4)

After a failed chat turn the note stayed up with nothing to close it, one tab away from the chat its own advice points to ("pick another one above the chat box").

  • The note gets the finished card's "OK" button, which goes back to the chat.
  • Left alone for ten seconds, it folds the island. It does not go back to the chat by itself: the chat takes the keyboard, and nobody asked for that.
  • A note with its own timing (a failed drop, gone after 2.4 s) is unchanged: no button, no second timer.

On the real app: OK lands in the chat. With the island's own auto-close set to 120 s, so that only the note's timer could act, the note was still up after 6.5 s and the island was folded after 12 s.

6. The picker marks a model its provider refused

A provider's list offers models a key cannot use. Gemini lists gemini-2.5-pro to accounts it then answers 404 to, and afterwards the list still showed it like any other.

The chat now remembers the models a cloud provider answered 404 for, and the picker shows them greyed with "Not available". They can still be picked, and one that answers again loses the mark. Only a 404 marks a model: quota, rate-limit and server errors can pass, so they do not. It covers the providers in openai_compat.rs; Anthropic and the model servers are untouched.

7. A refused model stays marked between launches (needs 6)

With commit 6 alone the marks are gone after a restart. They are now kept in refused-models.json, next to the log. The file is written only when a mark is added or removed, never on an ordinary answer, and past 200 marks the oldest go.

On the real app: three Gemini models that answered 404 were still reported as refused by a fresh launch.

8. The picker shows the providers that are set up

Every cloud provider is listed, key or not, and picking one without a key leads to "No API key". Model servers already only show once connected. This treats cloud providers the same way: one shows once its key is stored, and the active one always shows, so the picker never hides where the chat goes. With nothing set up, the active provider still says its key is missing and links to Settings.

On the Mac, ModelPickerView always shows the cloud providers, so this departs from it. Drop this commit if you want the two alike. If you want it on the Mac too, I can add the same filter there.

Before (OpenAI has no key) After

Tests

What was applied to main npm test tsc --noEmit
All eight 335 pass clean
2 alone 324 pass clean
3 alone 326 pass clean
4 alone 324 pass clean
4 + 5 331 pass clean
6 alone 325 pass clean
6 + 7 325 pass clean
8 alone 325 pass clean
  • cargo test --workspace: 200 + 30 pass, run on the full series only.
  • Commit 1 is Swift: compiled and run on a macOS runner on my fork (table above).
  • By hand on Windows 11 with a release build, including the real Gemini 404 for commits 4 to 7. Every screenshot is the real app, captured the same way before and after.

Not tested

  • Linux: not run. It shares the Windows code path, and CI builds it.
  • Mac: no Mac app code changes, so there is nothing to run there beyond commit 1's test.
  • Commit 3's animation: I measured the island's height before and after and looked at stills. I did not judge the grow and shrink motion by eye.

🤖 Generated with Claude Code

@shakibbinkabir shakibbinkabir changed the title Windows & Linux: four fixes for the chat's model picker and its errors Windows & Linux: fixes for the chat's model picker and its errors Oct 8, 2026
@shakibbinkabir shakibbinkabir reopened this Oct 8, 2026
shakibbinkabir and others added 8 commits October 9, 2026 04:28
Two cases sleep for a fixed time and then assert that a pending timer has not
fired yet, with 150 ms and 100 ms to spare. On a busy macOS runner the sleep
runs long, the timer fires first and the precondition traps: line 27 on the
windows-catch-up PR, line 54 on mochi-colours, neither of which touches the
state machine.

Both timers are now a full second out, so the sleeps only have to outlast what
they are there to outlast. The test takes about 1.6 s longer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Infinite CSS animations were paused only in views that are not `.on`. The
view that is on stays on while the island is compact or hidden, so the
ticker's shimmer went on repainting at 60 fps inside an island of zero height.

`#content` now carries `.live` while it is visible, and everything under a
content that is not live is paused. Nothing visible changes: the content has
opacity 0 whenever it is not live.

Measured on Windows 11 with the island hidden after a Claude Code session,
CPU time of coucou.exe plus its WebView2 processes over 30 s:
before 2,297 ms (renderer 1,344 + GPU 797), after 656 ms (GPU ~0).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The model picker is a popover on macOS, with 260 pt for the list. Here it sits
inside the island, in whatever the chat leaves: with an empty conversation the
island is 240 px tall and the list had room for one model, scrolled with the
arrows of a collapsed scrollbar.

While the picker is open the island now takes all the height the panel has
(312 px), the way the question view already grows for its options, and comes
back to the chat's own height when it closes.

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

A chat error is shown as a note, in a card made for one short line. The cloud
providers' errors carried the provider's whole message after our own sentence:
Gemini's 404 for a model new accounts can no longer use adds three sentences
of advice for developers and a link, and the note filled its card edge to edge.

- net::error_detail keeps the provider's first sentence (it already promised a
  text "cut down for a message", but only cut plain-text bodies).
- The note is held to four lines with an ellipsis, with the whole text in its
  tooltip, so whatever is passed to it stays inside the card.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a failed chat turn the note stayed up with nothing to close it: the
only ways out were the tabs or Escape, and the chat it came from was a tab
away from the advice it had just given ("pick another one above the chat box").

- The note gets the finished card's OK button, which goes back to the chat.
- Left alone for ten seconds it folds the island. It does not go back to the
  chat by itself: the chat takes the keyboard, and nobody asked for that.
- A note with its own timing (a failed drop, gone after 2.4 s) is unchanged:
  no button, no second timer.

Builds on the previous commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A provider's list offers models a key cannot use: Gemini lists gemini-2.5-pro
to accounts it then answers 404 to. Nothing says so before the model is used,
and after the error the list still showed it like any other.

The chat now remembers, until the app quits, the models a cloud provider
answered 404 for, and the picker shows them greyed with "Not available". They
can still be picked, and one that answers again loses the mark. No request is
made to find out: only what a real chat turn was answered is remembered.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The models a provider answered 404 for were only remembered until the app
quit: after a restart the list offered them again like any other, and the
only way to find out was to hit the same error.

They are now kept in refused-models.json, next to the log: model names only,
nothing leaves the machine, and the uninstaller removes it with the log. A
model that answers again loses its mark for good, and the file is only written
when a mark is added or removed, never on an ordinary answer. Past 200 marks
the oldest go.

Builds on the previous commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The picker listed every cloud provider, with or without a key, next to the
model servers, which only show once connected. It now treats them alike: a
cloud provider shows once its key is in the credential store, a model server
once it is connected, and the active provider always, so the picker never
hides where the chat goes. With nothing set up, the active provider says its
key is missing and links to Settings, as before.

The store is only asked whether each key is there, when the picker is built
and each time it opens, so a key added in Settings shows the next time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2c227d4a2c75feba4fb1cf99c996e2e83bbdd06e)
@shakibbinkabir shakibbinkabir changed the title Windows & Linux: fixes for the chat's model picker and its errors Windows & Linux: eight fixes from daily use, one commit each (idle CPU, the chat's picker and errors, a flaky test) Oct 8, 2026
PythonTilk added a commit to PythonTilk/coucou that referenced this pull request Oct 9, 2026
Taken from upstream PR Louis-CFM#318 by shakibbinkabir (folded into Louis-CFM#365 there): the
ticker's shimmer kept repainting while the island was compact or hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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