4.3.6 and 4.3.7: grey text in the composer, five sign-ins each, one token for many repositories
Two small releases. The chat composer now suggests the rest of what you are typing, half from a word list that never leaves the webview and half from the chat model; the gateway counts sign-in requests per developer behind a reverse proxy instead of per proxy; and the pull request plugins keep one copy of an access token, so an expired one is replaced once.
4.3.6 shipped on 6 October and 4.3.7 on 7 October. One is an extension release with a gateway fix attached, the other is a twinny-server release the extension only carries the number of. Neither is large. Both change something you touch every day if you use the thing they touch, so this is what they do and where the lines are.
Grey text in the composer
Type in the chat composer and grey text appears after the cursor, as an inline completion does in the editor. Tab takes all of it, Ctrl+→ takes a word, Esc puts it away. That first Esc does not count towards the two-press clear of the draft; the ghost-text extension in src/webview/ghost-text.ts eats it when there is something to put away. The keys are the composer’s own, so Ctrl is Ctrl on macOS too. twinny.chatSuggestions turns the whole thing off.
There are two sources, and the distinction matters for a privacy-first extension.
The first is a word predictor in src/common/composer-predict.ts, a plain function with no model behind it. It builds a table of words and a table of which word followed which from the prompts you have sent, the last fifty kept in the webview’s local storage plus the user turns of the open conversation, with code blocks stripped and sentence ends breaking the chain. A short seed list of words anyone asks a coding assistant (function, explain, refactor, workspace) gives it something to offer before you have typed anything, but a word you have typed yourself counts for twenty seed words. From two letters of a partial word it offers the rest of the likeliest match; after a space it offers the word that has followed the previous one at least twice. It keeps your case (PROV becomes PROVIDERSELECT) and it leaves a word alone when what you typed is itself likelier than any longer word, so the is not pushed towards there. This part runs on every keystroke and sends nothing anywhere.
The second is the chat model, and it only joins in once the conversation has a reply. After a space or a comma, semicolon or colon, and a 500 ms pause with no key, suggestDraft in src/extension/chat/index.ts sends the draft and the last six messages, three exchanges, to the active chat provider. Each message is cut to 400 characters and code blocks are replaced with [code]. The request asks for at most 32 tokens at temperature 0 with thinking off, a new keystroke cancels the one before, and nothing is sent while a reply is streaming, because the two would share the GPU. If your chat provider is hosted, this means half-typed drafts leave the machine; if it is Ollama on the desk, they do not. That is the line to know about.
The prompt is the interesting part. A chat model asked about an unfinished message answers it, because that is what chat models do. So the system prompt tells it that it is the predictive text on a phone keyboard and must never answer, and it is shown two worked examples before the real one. cleanModelSuggestion then takes the first line of the reply, strips quotes, strips the draft said again or its last few words restarted, drops anything that opens like an answer (Sure, You can, Here is), and cuts at the end of the first sentence or 80 characters. Whatever survives is shown, and stays shown for as long as what you keep typing agrees with it. The tests in src/test/standalone/composer-predict.standalone.ts are the readable spec for all of this.
Five sign-ins per developer, not per proxy
POST /twinny/v1/signin is a route that needs no key: a developer asks to join, gets a code, and an admin approves it within ten minutes. To keep that route from being a nuisance, src/gateway/signin.ts allows five waiting requests per client address and a hundred in all.
It took the address from the socket. docs/gateway.md tells you to run the gateway on 127.0.0.1 behind nginx or Caddy, and behind that deployment every developer arrives from the proxy’s loopback address. So the whole team shared one allowance of five, the sixth person to sign in was told “Too many sign-in requests are waiting from this address”, and anyone could hold sign-in shut for everyone for ten minutes by asking five times.
4.3.6 switches the route to clientAddress(), the same function the audit log, guest invites and page links already used. The rule is narrow on purpose: X-Forwarded-For is believed only when the peer is loopback, and then the last hop in the header is taken, which is the one the proxy on this machine appended. A header sent straight to an exposed gateway is ignored. The test added in src/test/suite/gateway-signin.test.ts checks that behind a loopback proxy the cap counts the forwarded address, not the proxy.
One token, many repositories
The GitHub, GitLab, Gitea and Bitbucket plugins watch repositories for pull requests to review. Until 4.3.7 each repository kept its own copy of the token pasted with it, which was fine for one repository and a chore for twelve: when the token expired, every repository had to be removed and added again.
A plugin page now has a Tokens panel. A token is saved once, with a label, and each repository picks one in its reads with column. One classic token for everything, a fine-grained token per organisation and a token per repository are all just rows. Saving a token can ask the host what it reads (GET api/tokens/<id>/repos) and tick several repositories at once.
Replace is the point. updateToken in src/gateway/plugins/pulls.ts checks the new value with the host, then proves it against the first repository on the token before anything moves, and only then swaps the value and syncs every repository on it in the background. A host answering 401 on a sync marks the token and each repository on it as refused, and the page highlights that token’s replace button. The page itself only ever receives a TokenView: id, label, who the host said it was, when it was added and replaced, how many repositories use it and how many are refused. Never the value. Developers the plugin is shared with never see the tokens.
The migration is in parseReposFile in plugins/forge.ts. On first start, inline tokens in repos.json are folded into saved tokens, with the same value pasted for several repositories becoming one token, and repository ids are kept, so reviews and triage records stay attached. The one-way part: an older twinny-server cannot read saved tokens, and the first change it writes drops them from the file. Back the file up before rolling back; the changelog says so for a reason.
What’s new per release is at docs.twinny.dev; the composer keys are on the chat page and the Tokens panel on the plugins page.