Skip to content

Pin the paste to a window from the compact pill - #15

Merged
samartomar merged 3 commits into
mainfrom
pin-window
Sep 27, 2026
Merged

samartomar merged 3 commits into
mainfrom
pin-window

Conversation

@samartomar

Copy link
Copy Markdown
Owner

The compact pill gets a pin. Tap it and every paste goes to the window you were in, whatever's in front when you speak.

The case it came from: writing feedback in Notepad while reading the site it was about. Every dictation pasted into the browser, and Paste last could only move it afterwards, one paste at a time.

What it does

The pin sits at the right end of the capsule. A tap pins paste_target, the window the pill already tracks. While it's pinned, a Type paste, Refine's Send, Paste last and a spoken correction all land in that window. _hand brings it forward, pastes, waits PIN_SETTLE_SEC (0.12 s) and hands the foreground back to where you were.

Windows only lets the process with the last input event move the foreground, and after the talk keys that isn't Flow. So inject.bring_forward asks plainly first, and when that's refused it attaches to the front thread's input for one SetForegroundWindow, then detaches. It restores a minimised window first. inject.window_alive checks the pid as well as the handle, because Windows reuses a closed window's handle, and closing the pinned window unpins it.

A pinned paste run survives keys and clicks elsewhere, so "change X to Y" and joins still work after you've clicked around the browser.

How it looks

The icon is a solid pushpin, head up and right. Unpinned it's slashed and grey, pinned it's whole and in the mic's tint. It's the one filled mark in glyphs.py, and test_glyphs lists it as the fill rule's only exception. It's 14 px, 9 px from the capsule's end, so the needle clears the last meter bar.

While a window is pinned the strip always says "pinned to X - words go there wherever you are", coming back after any other notice.

Lite draws no pin, and the Classic pill and the right-click menu are unchanged.

Tested

Samar tried it on real windows: "it works great". The full suite ran on this branch rebased onto d7a9d4b (3239 tests). The only failures were the 13 known test_refine ones from this machine's codex.CMD shim, plus the sdist size guard, which trips locally on an untracked folder. The committed tree's sdist is 1,939,421 B, under the 2 MB guard.

Still to come in a later change: a guide.md section and a decisions.md entry, since this adds a gesture to the pill.

🤖 Generated with Claude Code

samartomar and others added 2 commits September 27, 2026 12:01
Asked for by the owner, who was writing feedback in Notepad while reading the
site it was about: every dictation pasted into the browser in front, not into
Notepad. Paste last could move it afterwards, one paste at a time.

The compact pill now has a pin at the right end of the capsule, in the room the
meter leaves there. A tap on it pins the paste to the window you were in:
paste_target, which the pill tracks and which a tap does not move, since the pill
never takes the foreground. While it is pinned:
- **Every paste goes there, whatever is in front.** That covers a Type paste,
  Refine's Send, Paste last and a spoken correction. `_hand` brings the pinned
  window to the front, pastes, waits PIN_SETTLE_SEC for the keystrokes to
  arrive, and gives the foreground back to where you were. `inject.paste` still
  refuses a window that is not in front; the pinned one simply is, by then.
- **The pin wears the mic's tint,** where it is grey unpinned, and the strip says
  once which window it holds. A second tap lets it go, and so does closing the
  window. A hold on the pin still talks; only a tap there is the pin's.
- **A pinned paste stays changeable while you are elsewhere.** Keys go to the
  window in front, and a click on the pinned window brings it there, so nothing
  done elsewhere can move its caret. The run ends by the usual rules once you are
  back in that window. So "change X to Y", and the space between one note and
  the next, still work after you have clicked around the browser.

Windows lets only the process with the last input event move the foreground,
and after the talk keys that is not Flow: the chord runs on a keyboard hook. So
`inject.bring_forward` asks plainly first. When that is refused, it shares the
front thread's input for one SetForegroundWindow and detaches straight after.
It restores a minimised window first. `inject.window_alive` checks the process
id as well as the handle, because Windows reuses a closed window's handle.

The pin is also in `_draw_key`: a frame redraws only when the key moves, and
without it a pinned Type pill was photographed still grey. Lite draws no pin,
because it has no windows to tell apart. The Classic pill is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Samar tried the pin on real windows ("it works great") and asked for two changes.

**The line stays.** "pinned to Notepad - words go there wherever you are" was the
strip's usual three seconds, and where the words are going is the one thing the pill
cannot show by colour alone. While a window is pinned the strip is never empty. The
line comes back whenever another one ("not pasted: ...") has had its turn, and once
unpinned the strip goes as it always did.

**The icon is the pushpin Samar sent**, a solid pin lying head up and right, and the
same pin slashed for "not pinned":
- **Unpinned:** slashed and grey.
- **Pinned:** whole, in the mic's tint.
- **The cut:** a line over a solid shape in its own colour disappears into it, so
  the slashed pin is cut into two pieces around the slash. The cut is sized by the
  stroke in pixels, so it stays open at every size.
- **Geometry:** drawn upright in its own units and turned 45 degrees into the box,
  so the numbers are the pin a person would sketch.

It is the one filled mark in glyphs.py beyond a dot. test_glyphs names it as the
fill rule's only exception (FILLED), and the pin still meets every other rule of the
language: colour, tags, box and stroke weight.

**Size and place:** 14 px rather than 12, which read as a smudge. It sits 9 px from
the capsule's end, so the needle clears the last meter bar; at 94 the photograph
had the two touching. The test for it now reads the capsule's round end rather than
a margin. The meter's test helper tells a bar from the pin's grey body by width.

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

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T17:09:47.505098Z 3df5497 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3df5497e00

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread flow/ui_compact.py Resolved
Comment thread flow/ui_compact.py Resolved
Comment thread flow/ui_compact.py Resolved
… the pin at the paste

Codex's three findings on #15, and the macOS leg's one error.

- A tray Paste last while pinned first gave the foreground to
  `paste_target`, which pinned is the pinned window, so `_hand` had nowhere
  to go back to and Notepad stayed in front of the site you were reading.
  While pinned, `_track_target` now keeps `_last_front` (never Flow's, never
  the taskbar, `classify` on the edge only), and the tray's restore uses it.
  Pinning starts it at the pinned window, not one from an older pin.
- `_hand` asks `window_alive` with the pid again right before the paste. A
  window closed since the last frame, its handle maybe another's already,
  gets nothing, and the pin lets it go.
- The hand-back after PIN_SETTLE_SEC only happens while the pinned window
  still has the foreground, so a window you picked meanwhile stays in front.
- The test Desk fakes `classify` too. Unpinned, a frame asked the real one,
  and on a Mac that imports `flow.inject`, which has no WinDLL to bind.

Suite 3243, the 13 known test_refine failures only (codex.CMD on this
machine). Each new test fails on the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@samartomar
samartomar merged commit 25e5984 into main Sep 27, 2026
4 checks passed
@samartomar
samartomar deleted the pin-window branch September 27, 2026 17:26
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