Skip to content

Releases: malinborn/couplet

v2.3.1 — short rows reach the edge

Choose a tag to compare

@malinborn malinborn released this 06 Oct 14:12

couplet 2.3.1

Short rows reach the edge.

  • Empty cells are drawn. A table row with fewer cells than the widest one stopped at its last cell — background, border and stripe included — so a sparse table read as a staircase. Short rows are now padded with empty cells to the full width, the way GitHub renders them. Double-click one in a data row and couplet writes the missing cells into the row and opens it for editing. The header's padding stays read-only: in GFM the header must have as many cells as the |---| line under it.
  • Rows without outer pipes keep their text. GFM lets a row leave off its leading or closing |, but couplet's cell reader ignored whatever sat outside them: | R-091 drew an empty row, and a | b | lost a. Both ends are read now, the same way the markdown parser reads them.

Upgrade: brew update && brew upgrade --cask couplet

v2.3.0 — CSV opens as a table

Choose a tag to compare

@malinborn malinborn released this 05 Oct 15:10

couplet 2.3.0

CSV opens as a table.

  • .csv and .tsv are tables now. Open one and the window holds the same table widget markdown gets: edit cells, add and drag rows and columns, and the file is saved back as CSV. The delimiter (, ; or Tab), a BOM and the line endings are read from the file on every save, so an Excel export stays an Excel export. couplet also shows up in Finder's "Open With" for both. couplet edit refuses CSV files for now; show works.
  • Huge CSVs open as text. Above 20,000 rows a CSV opens read-only, with a note saying why. 100,000 rows as a table meant about 1.6 million DOM nodes and a frozen window; a scrolling table that draws only what is on screen is the next step.
  • Big tables got fast. The striped rows used :nth-child(even of …), which WebKit recomputes from scratch for every row — quadratic, 4.6 s of style work on 20,000 rows, now 0.5 s. A cell edit rebuilt the whole table; it now patches the cells that changed: a commit in a 10,000-row table went from 1.7 s to under 0.3 s. Markdown tables render up to 1,000 rows (was 500).
  • The header stays put. Scroll down a long table and its header row sticks to the top of the window until the table ends. It never could: the table clipped its rounded corners with overflow: hidden, which quietly made it the box a sticky header sticks to.
  • Code files, two fixes. A .py (or any code file) opened inside a grey box that vanished on the first click — its style hung on a class the editor rewrites on every focus change. The box is gone for good. And Enter after def f(): indented by two spaces: nothing ever set an indent unit. couplet now reads it from the file (four spaces, two, or tabs), or uses the language's convention when the file has none.
  • Smaller table fixes. Enter off the last visible row no longer opens the next cell's field below the window's edge, and a markdown table too long to render keeps its caret and its search hits instead of throwing the caret out.

Upgrade: brew update && brew upgrade --cask couplet

v2.2.0 — Find shows what it found

Choose a tag to compare

@malinborn malinborn released this 03 Oct 07:31

couplet 2.2.0

Find shows what it found.

  • Matches you can read. A match inside a heading used to be an empty box: heading text is painted through its gradient with a transparent fill, and the match background landed on top of the glyphs. Inside inline code the code's own background covered the match, and inside a table nothing was highlighted at all. All three now show real text, and the current match, the other matches and the selection are three different colours — one shared !important rule used to paint them identically. A test holds every theme to a 4.5:1 contrast for match text.
  • Everything else steps back. While the cursor is in the Find field, the page dims except for the matches, and the current one glows. Click back into the text and the dimming lifts; the highlights stay.
  • How many, and which one. A new Find panel: 3 / 17 right in the field, toggles for case, regexp and whole word, a replace row on demand, and a red field when nothing matches or the pattern is broken.
  • ⌘G / ⌘⇧G. Find Next and Find Previous are in the Edit menu and printed next to the arrows in the panel. They always worked; nothing said so. With the tab drawer open, ⌘G is still "To a window…".
  • Stepping through tables. Landing on a match in a table row snapped the caret out of the table, so the next step skipped every other match in it. Search steps now stay in the row, and the current match is outlined in its cell.

Upgrade: brew update && brew upgrade --cask couplet

v2.1.1 — ⌥⌫ and ⌘⌫ work in the drawer filters

Choose a tag to compare

@malinborn malinborn released this 27 Sep 18:06

couplet 2.1.1

A one-fix patch for the drawer filters.

  • ⌥⌫ deletes a word, ⌘⌫ clears the filter. The tab and stash drawers filter as you type, but only a plain ⌫ edited the query. ⌘⌫ fell into the branch that handles ⌘L/⌘R/⌘U, found no sort for it and did nothing; ⌥⌫ was read as a plain ⌫ and took one character. Both now behave as in any macOS text field, in the trash view too; on an empty query they do what ⌫ always did.
  • ⌫ takes a whole emoji instead of half of one.

Upgrade: brew update && brew upgrade --cask couplet

v2.1.0 — the stash: nothing you type is untitled, and nothing you type gets lost

Choose a tag to compare

@malinborn malinborn released this 27 Sep 16:38

couplet 2.1.0

The stash: nothing you type is untitled, and nothing you type gets lost.

  • Untitled is gone. A new tab becomes a note the moment you type its first character — a plain .md file in ~/couplet/, named by date, titled by its first line. Close it with ⌘W, the red button or ⌘Q and it is put away in the stash, not thrown away; a tab you never typed in just disappears. Drafts from earlier versions are imported as notes on first launch — read back and compared before the old copy moves aside.
  • The stash drawer. ⌃S opens it beside the tab drawer, ⌃T puts the current tab away, and a file you tag or put away is kept as a reference — the file itself stays where it was, untouched. Drag cards between the two drawers, sort by changed, opened or kind, filter by #tag; a project window opens the stash already filtered to its repository. A narrow window widens itself so the drawers never overlap, and shrinks back when the stash closes.
  • Search that finds word forms. A trigram index over titles and text, so тайник finds тайнике and мент finds документ, case-insensitive in any alphabet. Titles weigh more than text; the matched spot is highlighted and the card shows the text around it.
  • A trash that keeps things for 30 days. Deleting a note moves its file to ~/couplet/.trash/; Trash at the bottom of the stash restores it with its tags or deletes it for good. A note open in another window is released by that window first, after its last keystroke is on disk, so the next autosave cannot bring it back. Removing a file reference never touches the file.
  • Why 2.0.1 lost drafts on upgrade. Restoring the previous session is opt-in, and a launch nobody restored rewrote the session file with only the windows it had open; the next launch saw the old drafts as unreferenced and deleted them. The session now keeps every un-restored window that holds a draft, and nothing is deleted outright: unreferenced drafts move to a trash for 30 days, and a draft that cannot become a note leaves a rescue copy when it is closed.
  • Your agent can use the stash. couplet stash search | list | get | add | tag and the same five MCP tools. The default scope is the repository the agent works in; search returns short snippets and get one note at a time, so an agent never pulls the whole stash into its context. Run AI → Teach Your AI couplet again to tell it.
  • ⌘' / ⌘⇧' walk through AI marks — the next or previous edit your agent highlighted, comment anchor or open question, wrapping around and unfolding whatever hides it. Menu shortcuts, so they work with focus in a comment box.
  • Quotes render what is inside them — bold, links, code, lists, checkboxes, headings and nested > levels, each with its own bar, muted towards the quote grey. Tables and mermaid inside a quote stay source.
  • Crossing formatting nests instead of tearing. In Live Render, italicising text that ran past the end of a bold word wrapped the hidden ** with it, and the repair filter added a stray **. The selection is now split at hidden markers it crosses, and spans of the same kind merge.
  • Tab cards say when a tab was last viewed or edited; View → Tabs → Show Dates hides it.
  • Autumn — cinnamon and pumpkin by day, a violet sunset at night.

Upgrade: brew update && brew upgrade --cask couplet

v2.0.1 — ⌃Tab switches tabs from the keyboard

Choose a tag to compare

@malinborn malinborn released this 25 Sep 22:11

couplet 2.0.1

A one-fix patch for tabs.

  • ⌃Tab switches tabs from the keyboard again. In 2.0.0 View → Tabs → Show Next Tab worked when clicked and did nothing when pressed. The cause was the kind of shortcut, not the key: a Control-only menu shortcut never reaches the menu while the window is active — the page takes it as an ordinary keypress (⌃[ failed the same way in a trial build). ⌃Tab / ⌃⇧Tab are now handled by the page itself, the way ⌃1…⌃9 always were.
  • The menu now shows ⌘⇧] / ⌘⇧[ for Show Next / Previous Tab — the same keys as Safari, Chrome, Terminal and Finder. Both pairs do the same thing. A test fails the build if a Control-only shortcut ever lands in the native menu again.

Upgrade: brew update && brew upgrade --cask couplet

v2.0.0 — md-mini is now couplet, and it brought tabs

Choose a tag to compare

@malinborn malinborn released this 25 Sep 10:51

couplet 2.0.0

md-mini is now couplet — and it brought tabs.

brew update && brew upgrade --cask mdmini

That command still works and moves you onto the couplet cask. New installs: brew install --cask couplet.


md-mini is now couplet

There are two apps called mdmini, and the other one has mdmini.com. Two small editors under one name meant confused searches, downloads of the wrong thing and bug reports about someone else's program, so we stepped aside rather than fight over it. The name also stopped telling the truth: this began as a minimalist markdown editor and became a page you and your AI agent work on together. A couplet is two lines that read as one.

  • Your data moves on first launch — windows and tabs, caret positions, unsaved drafts, crash copies, theme and recent files go from md-mini's folder into couplet's, and md-mini's folder keeps a note saying where they went. If md-mini is still running, couplet asks it to quit first instead of copying data out from under it.
  • The old name keeps working — mdmini is an alias for couplet, and coup is the short one, so scripts, aliases and $EDITOR need no edits. An MCP server registered as mdmini still answers with the same tools; it now introduces itself as couplet.
  • Restart your agent sessions — a session opened before the upgrade still talks to md-mini's socket and will not see couplet until it restarts.
  • Your agent moves over in one step — AI → Teach Your AI couplet hands over a prompt that now also cleans up the md-mini setup: it registers couplet instead of mdmini, replaces the old skill (and keeps it if you edited it by hand), rewrites the note in CLAUDE.md, and tells you what it changed.
  • Installed from the DMG? Delete /Applications/md-mini.app. Your data is already in couplet, and the old app, opened by accident, would start empty. An MCP registration that points at a path inside md-mini.app stops working: re-register with claude mcp add --scope user couplet -- couplet mcp.
  • couplet watch lines start with [couplet] instead of [mdmini] — worth knowing if a filter of yours matched on it.

Tabs

  • Windows hold tabs — ⌘T, ⌘W (the last tab closes the window), ⌘1…⌘9, ⌃Tab / ⌃⇧Tab. A file lives in one tab across the whole app, so opening it again focuses it instead of making a copy.
  • Windows are numbered — the title reads README.md — #7, and the notch on the left edge shows the number and how many tabs the window has. ⌘J opens the tab drawer; the notch glints while a tab your agent opened is still unread.
  • ⌘⇧T brings back what you closed — the last tab, into its window, caret included. The Window menu gains Minimize (⌘M) and Reopen Windows from Last Session.
  • ⌘Z stays in its own document — after ⌘O or a Recent file, undo could write the previous document's text into the new one: the inverse of a deletion survived the swap. The history is rebuilt on every switch now, and the last ≤300 ms of typing is saved before it happens instead of dropped.

Six languages

English, Spanish, German, French, Russian and Simplified Chinese, taken from your system on first launch, with Language and Check for Updates… in the app menu. Changing the language restarts the app: rebuilding the native menu in place would have meant four new places for a silent mismatch.

Two more themes, and the Dock follows them

  • Paper — ink on paper, and the same paper at night in sepia; coral only for what you change: the caret, the checkbox, the line through a finished task. Ink — cream on ink with a coral caret that glows, and ink on a cool light sheet for the light half.
  • The Dock icon takes the theme's colours — both halves of all six families, following Follow System too. The Finder icon stays the default one: the app is ad-hoc signed, and writing into the bundle would break the signature.
  • /theme in the slash menu switches themes live as you arrow through the list; Enter keeps it, Esc goes back.

Fixes

  • Zoom scales the whole window — ⌘+/⌘− set a font size that the editor overrode, so the only thing that ever grew was the slash-menu tooltip. It is page zoom now: text, tables, diagrams and toasts scale and rewrap to the window.
  • Files with Windows line endings open — a CRLF file opened as an empty Untitled window with no error: the caret was placed past the end of a document CodeMirror had already shortened. Line endings are normalised on read and restored on save, and a failed open or reload now says so.
  • No "External Change" dialog on your own saves — your own save was recognised by a 600 ms timer that could lose the race to the file-system event. It is recognised by content now.
  • couplet --version prints the version — it used to open an empty window.
  • Dragging across part of a heading in Live Render no longer flickers between the fragment and the whole line.

v1.3.0 — four themes, Live Render out of beta, OCD Alignment

Choose a tag to compare

@malinborn malinborn released this 21 Sep 20:30

mdmini 1.3.0

Four themes, a renderer out of beta, and a cross for everyone who cannot unsee the tick.

brew update && brew upgrade --cask mdmini

Live Render is the default, and no longer a beta

The mode that hides markdown syntax outright is what you get on launch now, including if you never opted in.

All three engines are still there under View → Editor Engine, spelled Raw, Preview and Live Render. The word "Live" in "Live Preview" only ever existed to contrast with the beta, and there is nothing left to contrast with.

Someone sitting in Raw stays in Raw. That is a decision to look at the source, not a rendering preference to overrule — but the next ⌘E brings them into Live Render.

⌘E stopped rotating through renderers

Live Render and Preview are two answers to "how should the markup look", and you pick between them once, in a submenu. ⌘E asks something else entirely — "show me the source" — so it now flips between Raw and whichever renderer you chose, and lands back where you left.

The "Include Live Render in ⌘E" switch that used to make it a three-way rotation is gone.

Two new themes, and neither is a port

Blueprint is a cyanotype. White on deep blue, because white there is not an accent but unexposed paper — and red is reserved for what you change: the caret, the checkbox, struck-through text. Its light half is tracing paper with a blue pencil.

Phosphor is a terminal from before colour. P1 green on black glass, amber as the only second hue (the other phosphor, from the terminal next to it), and monospace in the prose itself. Its light half is a fanfold printout.

The Theme menu is three groups, not a list

Family (Classic, Aurora, Blueprint, Phosphor), half (Light, Dark), and Follow System as a checkbox.

"System" used to be a fifth mutually exclusive option, which is the one thing it is not: it decides the half, and it has nothing to say about which family you like. Tick it and the theme follows your Mac from day into night without leaving the family you chose.

"Default" is now called Classic — the name it has had in the code since it was written.

A ticked task reads as done

Greyed and struck through, while bold stays bold, italic stays italic and a link keeps its underline. Only the colour is taken over.

A wrapped item stays greyed across its continuation lines; an unfinished child of a finished parent does not. The range follows the item, not the line.

In the Aurora themes the grey is a gradient. In Blueprint the line is drawn in that theme's red pencil while the text stays muted: crossed off, not deleted.

One menu item where four documents used to be

AI → Teach Your AI mdmini.

"Getting Started", "Connect via CLI", "Connect via MCP" and "Teach your AI" each explained their own part and left the assembly to you, so setting this up meant stitching a configuration out of four texts.

The document now hands over a prompt instead. Your agent registers md-mini over MCP, writes itself a skill on how to use it well, and adds a short note to its own config about when to reach for it — knowledge in the skill, which loads only when needed, and only the trigger in the config, which is read on every run. It is told plainly to prefer MCP and fall back to the mdmini CLI when MCP is unavailable.

A second prompt in the same document does the skill only, for anyone who would rather their main config stayed untouched.

OCD Alignment

The tick in a checked box sits a hair off centre. That is not sloppiness: ✓ is not a symmetrical glyph, so it never looks centred anywhere, whatever you do to it.

If that hair is going to follow you around all day, View → OCD Alignment replaces it with a cross drawn as two strokes rotated around one point — symmetrical by construction, with nothing left to drift.

We like the tick. We also know how it is.

Fixes

  • The table header stopped ignoring the theme. Its gradient was hardcoded in two variants, picked by whether the theme's name ended in "light" — so Phosphor got a lilac header on green paper. It is a theme variable now, with the old values as the default, so the themes that never asked look exactly as they did.
  • A checkbox in the menu works with two windows open. A menu event is broadcast to every window, and each applied it to its own copy of the setting. Harmless for a radio item, where N windows set the same value; not for a toggle, where N windows flip it N times. The event now carries the value rather than the instruction to flip.
  • Zoom In has a key again. It was declared as ⌘Plus, and accelerators are parsed by key code, where Plus is not one — the item simply had no shortcut. It is ⌘= now, next to ⌘−.
  • Open on an in-document link does something. In Live Render's link inspector it used to do nothing at all for a #heading target; it jumps to the heading.
  • The welcome page is about this version. It was still describing 1.0 and a menu that no longer exists.

v1.2.0 — Type in Live Render without breaking it, and edit tables like a grid

Choose a tag to compare

@malinborn malinborn released this 12 Sep 13:21

mdmini 1.2.0

Type in Live Render without breaking it, and edit tables like a grid.

brew update && brew upgrade --cask mdmini

Live Render survives being typed in

The mode hides the markdown markers, which meant every edit that touched one silently damaged the file. Backspace past a bold word turned **bold word** into **bold word.; Enter inside it left two unpaired ** on separate lines; a space after it produced **bold **, which CommonMark refuses to close, so the asterisks stopped being markup and became four literal characters on screen.

A repair layer now sits in front of the document. It tells "the user is deleting this whole span" from "the marker pair just got torn" by the shape of the change, and writes the marker back before it ever reaches disk.

Typing continues the format

The character after a bold word is bold, the way it is in every editor where you can see what you are doing. Esc or the format shortcut ends it.

Option+arrows and Option+Backspace step over the hidden markers instead of stopping on them, so a "word" is a word you can see.

The caret tells you the format

Thicker for bold, slanted for italic, a narrow stem with a crossbar for strikethrough, serifs top and bottom for inline code — and they combine.

With the asterisks hidden, the screen had nowhere left to say which format the next character carries, so the signal went to the one place the eye is already looking. It is right before the first character exists, too.

Tables are editable, not just renderable

A click parks the caret in the cell it landed in. It used to jump to the table's first character, so the next keystroke landed in the wrong cell and wrecked the row.

Cell text now selects, copies, takes bold/italic/code from the selection toolbar, and takes comments. The cell expands while you type instead of sliding under its neighbour.

Keyboard:

Key What it does
Tab / ⇧Tab Next / previous column, wrapping within the row
↵ Next row — and on the last row, out of the table
⇧↵ Line break inside the cell
⌘↵ Commit the cell
⌘⇧↵ Add a row

The ⓘ beside the wrap toggle carries the whole map, because a keyboard grid nobody is told about is a keyboard grid nobody uses.

A comment you never have to send

The reply area is always editable and saves itself as you type, so a half-written thought survives a crash, a reload, and the agent answering in the middle of it: an incoming answer freezes the previous turn without swallowing the sentence you are still writing.

The agent is no longer woken mid-phrase either — 20 seconds of quiet first, with the countdown running inside the send button so you can cut it short.

A comment lands where it was written

Anchoring was indexOf(quote): the first occurrence from the top of the file. A one-word quote, a list item, or a heading that also appears in a table of contents put the card on a duplicate — usually far above — and reported a confident match while doing it.

Threads are now anchored by the text around them. Measured over 20,962 cases generated from this repo's own markdown and then edited the way an agent edits a file (block inserted above, block deleted, 60 lines inserted far above, neighbouring lines rewritten, the anchored line duplicated three lines up):

strategy all ambiguous quotes only
first occurrence (before) 61.5% 13.0%
context anchoring (now) 100.0% 100.0%

Saving stopped damaging the file

An atomic write is a temp file plus a rename, and rename keeps the directory entry while dropping everything that lives on the inode.

  • Mode, owner, group, ACLs and extended attributes — quarantine included — are copied onto the temp before it moves into place.
  • Editing through a symlink writes to what the link points at, instead of replacing the link and writing to the wrong path.
  • A save the filesystem refuses now raises a toast. It used to fail in silence.
  • Comment sidecars go through the same path — they sit in the same protected, cloud-synced folders and carry the same metadata.

The atomicity itself is unchanged and now proven rather than asserted: a test really does SIGKILL the process mid-write and checks the file came back whole, old or new but never truncated.

JSON formatting stopped corrupting numbers

JSON.stringify(JSON.parse(s), null, 2) is not a formatter, it is a re-serializer: every literal was destroyed and rebuilt from a JavaScript double.

Input Before Now
{"id":12345678901234567890} 12345678901234567000 12345678901234567890
{"v":1e5} 100000 1e5
{"v":3.0} 3 3.0
{"v":1.500} 1.5 1.500
{"v":-0} 0 -0

It now re-indents the text it was given, so number literals, string escapes, key order and duplicate keys survive byte for byte — and the result lands in a ```json fence when the file is markdown.

Easier to read, in small places

  • Six heading levels you can tell apart, each theme fading its own gradient down the scale.
  • One indent step per list level, whatever the marker. The parser was the culprit, not the CSS: Tab always inserted two spaces, and CommonMark nests a sub-item only once its marker reaches the parent item's content column — 2 for -, but 3 for 1. and 4 for 10. — so an ordered sub-list never nested at all.
  • Tab indents every selected list item, not just the first.
  • Folded sections stop hiding text in Raw mode.
  • An AI edit highlights the regions that actually changed, rather than everything between the first and last of them.
  • Toolbar buttons name their keyboard shortcut.
  • [1] in prose keeps its brackets instead of being read as a link reference.

Known limitation

After an upgrade, macOS asks for folder access again. mdmini is ad-hoc signed — no Apple Team ID — so macOS identifies it by a code hash that changes with every release, and the permission you granted belongs to what it now considers a different app. Signing and notarizing properly costs $99/year, which a free open-source tool is not going to carry. Grant it again and carry on.

v1.1.0 — AI comments in the document, answered by your agent

Choose a tag to compare

@malinborn malinborn released this 24 Aug 13:27

You can now leave comments on your text for your AI right inside mdmini — works best with Claude Code.

Comments your agent answers

Until now the AI interface only ran one way: your agent could point at a line, edit the buffer, or ask you a question. Now you can ask back.

  • Comment on any text — select a fragment and press ⇧⌘M, or use the 💬 in Live Render's selection toolbar. The quoted words stay highlighted in the document, the card header carries an excerpt of them, and putting the caret in a commented fragment makes its cards shimmer — and the other way around. With several comments under one paragraph, that is the difference between finding the right card and hunting for it.
  • Your agent gets woken, not polled — AI → Connect Agent to Doc Questions copies a prompt that arms a watcher over your project. With Claude Code's Monitor tool, a new comment interrupts the session that already has your context, the moment you send it. Nothing here spawns an agent, and nothing hands your question to one that knows nothing about the work.
  • The answer comes back into the thread — mdmini answer plan.md --id c-4f2a1b. If the comment asked for a change rather than an answer, insert into text folds it into the document — highlighted like any other AI edit, Esc to dismiss, ⌘Z to take it back. If it was just a question, resolve closes the thread.
  • The comments live next to your file — in .mdmini_comments_<file>.md: plain markdown you can read, edit by hand and diff in git. md-mini never rewrites that file wholesale, only appends to it. So this works with the app closed, works for agents with no MCP, and needs no store of its own.
  • Git worktrees stop being a problem — comments sit beside the copy they were written on, so the agent watching that tree is the one that answers. No routing, no claims, no queue to divide between sessions.

Three new verbs, none of which need the app running: mdmini question <path> lists what is waiting, mdmini answer <file> --id <id> replies (text on stdin), mdmini watch <dir> streams a line per new comment. The MCP server exposes question and answer as tools. mdmini agent prints an updated block for your CLAUDE.md.

Also in this release

  • An answer folded into your text is now highlighted like any other AI edit. Text arriving from an agent used to look identical to text you wrote yourself.
  • md-mini.com has a new section showing the whole round trip — the menu, the terminal, the comment, the answer.

Upgrade

brew update && brew upgrade --cask mdmini