Skip to content

v0.2.3

Choose a tag to compare

@github-actions github-actions released this 07 Oct 16:33
· 17 commits to main since this release

dmCode v0.2.3

dmcode is a workspace now. The chat is still the agent you know; the same
window also holds a real code editor — project tree, git, a terminal, splits,
bookmarks and LSP — and switches between them with one key.

Install

# macOS / Linux / BSD
curl -fsSL https://raw.githubusercontent.com/dedomorozoff/dmcode/main/install.sh | bash
# Windows (PowerShell)
irm https://raw.githubusercontent.com/dedomorozoff/dmcode/main/install.ps1 | iex

Or from a checkout:

make install       # -> $(go env GOBIN), or GOPATH/bin

Or with Go 1.26+:

go install github.com/dedomorozoff/dmcode@latest

What's new since v0.2.2

The editor draws pictures instead of their bytes

Open a PNG and the editor used to show you the file's bytes as prose: a column
of ÿØ boxes, one per pixel row, that scrolled like source and — this is the
part that mattered — saved back over the picture. ctrl+s on a .png wrote
whatever was in the buffer into the file.

A picture is now a tab of its own kind. It is drawn where the lines would be,
at the pane's width, out of the same half-block renderer the chat's preview
uses — one picture, one drawing routine, rather than two that differ by which
half of dmcode opened it. There is no gutter: line numbers beside an image
describe a document that does not exist. The caption on the last row carries the
name, the dimensions, the format and — for an animation — which frame you are
looking at, because a GIF with no frame counter reads as a flicker.

A picture tab is read-only, and that is enforced rather than promised. Typing,
enter, backspace, paste and ctrl+s do not reach the buffer: the key
handler for a picture drops them before the editing switch every text tab falls
into, and a paste — which arrives as one message carrying the whole text, and
would walk straight past a check written for key presses — is refused where it
lands. The keys that are about the workspace rather than the file keep working:
switch tabs with alt+←/alt+→, scroll a tall picture with the wheel or
pgup/pgdn.

Animated GIFs play

A GIF is not a still with a .gif extension — the motion is the content, and a
preview of frame zero is a preview of a video in the same sense. Every frame is
decoded and composited at open, then advanced on a timer.

Compositing is the part that is easy to leave out and obvious once you know: a
GIF frame is the rectangle that changed, drawn over the frame before it, and
the disposal method says whether the canvas is put back afterwards. Rendering
the frames as decoded shows an empty cell for every partial frame, which in a
real animation is most of them — so each frame is drawn onto the canvas the last
one left behind, with DisposalBackground and DisposalPrevious honoured.

The timer is one tea.Tick per frame, each scheduling the next, rather than a
goroutine with a clock of its own. That is what makes the animation stop by
itself: a closed tab, a switch to another file, or the chat screen covering the
editor all mean there is nothing to advance, and the chain ends with no cancel
path to get wrong. A flag keeps one animation from becoming two chains — every
message asks for a frame, and without it the picture would run at twice the
speed its own delays declare. A frame delay the file asks for is honoured, with
a floor and a ceiling: a GIF may declare zero, which is a strobe rather than a
picture, and a declared ten seconds is a picture that looks frozen.

Frames are capped at 240, and a file over 64 MB opens as text with the reason
on the status line — a worse view of a picture, and a far better outcome than
pulling an arbitrarily large file into memory.

A file named .png that is not one opens as text, and says why

The extension is the cheap first question and the bytes are the real one, so a
.png that does not decode is not a tab that refuses to open: it is the text it
actually is, with one line in the status line naming the reason. The same is true
of the other direction — a picture overwritten with text reloads as text rather
than going on showing the frame of a file that no longer exists.

WebP is in the list of extensions worth trying and has no decoder in this build,
exactly as in the chat: you get a named refusal rather than a filename being
treated as prose.

The status bar stops describing a document that is not there

Ln 1, Col 1 under a rendered image is a cursor that is nowhere, and LF UTF-8
is a question nobody asks about a PNG. A picture tab reports its dimensions and
format instead, in the single pane and in each pane of a split alike.

Everything else from v0.2.2 stands

The rest of what shipped in v0.2.2 is unchanged and still current. In brief: one
file arriving under two names and the code comparing them — which made the
workspace refuse its own files, made git operations fail on files that were
plainly in the tree, made the editor open a file twice, and made saving not
reload the tab, all fixed through internal/pathnorm; a closed terminal that
left its shell holding the project directory; a white picture that drew as a
field of dots because Go fuses multiply-add on arm64; Cline as a first-class
provider in /setup; and /setup no longer outgrows a 22-row terminal.

One file has more than one name

CI grew macOS and Windows runners in that release, and on the first run eight
tests failed. All eight were the same thing: the same file arrived under two
names, and the code compared the names.
On a developer's machine those names
are the same string — the temp directory, the checkout and the home directory
are all spelled the one way the process already spells them — so nothing here
was ever visible locally. Change where either side comes from, and it appears:

  • t.TempDir() says /var/folders/..., os.Getwd() says
    /private/var/folders/....
  • A Windows runner's %TEMP% is C:\Users\RUNNER~1, and the repository it
    walks up into is C:\Users\runneradmin.
  • A symlinked checkout is not the directory it points at.

What that cost, before this release:

  • The workspace refused its own files. write_file into a path inside your
    project could be turned away as outside the working directory, naming a
    directory the file was in. On macOS and on a Windows runner, this was
    constant rather than occasional.
  • Git operations failed on files that were plainly in the tree. Staging,
    unstaging, blame and the gutter's HEAD content all computed
    /private/var/.../a.go against a root of /var/..., got ../../.., and go-git
    refused it: invalid path "../../..": cannot use "..". The error arrives in
    plumbing vocabulary, several layers from the spelling that caused it. It now
    names both directories, in dmcode's words.
  • The editor opened the same file twice. A path typed by hand, a restored
    session and a row clicked in the finder all spell it differently, so
    ctrl+o on a file already open gave you a second tab, and every "is this
    already open" question downstream was wrong along with it.
  • Saving a file did not reload its tab. The watcher reports the name the
    operating system gave it; the tab holds the name the editor was opened with.

internal/pathnorm is the answer in two functions — Canonical puts a path in
the spelling this machine agrees on (links followed, 8.3 short name and letter
case on Windows, missing tail appended for a file that does not exist yet), and
Same is the comparison. The workspace boundary, the git layer, and the
editor's tab list and file watcher all ask it now, and the boundary still refuses
a symlink that leaves the tree — the retry is a fix for the spelling, not a way
out of the check.

Closing a terminal now ends the shell

ClosePseudoConsole signals the console and returns; it does not terminate the
attached process. Closing the process handle straight afterwards only drops our
reference to something still running. The shell lives on, a shell holds its
working directory open, and a directory with a live handle in it cannot be
deleted — so quitting the editor could leave a cmd.exe behind holding your
project directory. The child is now terminated by pid and waited for, with a
bounded wait: a shell that somehow refuses to die must not hang the UI on
shutdown.

A white picture stopped drawing as a field of dots

The ASCII preview picked its cell with three float multiplies, and Go fuses
multiply-add on arm64 — one rounding where an x86 build does three. For pure
white the fused sum lands a hair under 255, int() truncates rather than
rounds, and the brightest pixel in the picture drew as the second-darkest glyph
on a ramp that ends in space. An all-white image rendered as the emptiest
possible picture on a Mac; a pale photograph came out one step too dark. It is
integer arithmetic now, and the ramp length is folded into the numerator rather
than dividing twice.

One more provider, and /setup fits a small terminal

  • Cline is a first-class option in /setup. It is a gateway rather than a
    client-gated service: api.cline.bot serves plain OpenAI /chat/completions
    with tool calling, and wants an ordinary key from app.cline.bot. Nothing
    about it is faked, which is why it sits next to the others. The default model
    is anthropic/claude-sonnet-4-6, because that is what Cline's docs name for
    coding.
  • /setup no longer outgrows the screen. The panel's height budget counted
    its header as three rows when the header is one row that wraps to two at 72
    columns, so on a 22-row terminal the bottom border fell off. The header is
    measured now rather than counted — which fixes the palette and the model
    picker too, since all three go through the same helper. Sixteen providers
    cannot each be visible on a 22-row terminal however they are drawn, so what is
    asserted is the invariant that does hold: every option is on screen or counted
    by the overflow marker, and the panel fits.
  • OpenCode Zen's default model is big-pickle, not
    nemotron-3-ultra-free. Both places that name it — the wizard's option list
    and the keyed preset — have to agree, or the wizard sets up a different model
    than the key alone selects.

Every list marks the row under the cursor the same way

The command list, ctrl+p, the model picker and the GGUF browser already drew
the selected row with a triangle, so entering /setup from the palette changed
the marker on the way in. The provider list, /proxy, /lang, /sessions and
the ask_user options did the same. All of them use the triangle now, and the
rule lives on the helper they all render through, so the next list inherits it
instead of inventing a glyph. A question mark stays where it means something
else — the prefix of a prompt title, as in ? provider key.

The installers verify what they downloaded

Every release publishes a sha256sums.txt over its assets, and both installers
check their download against it before installing: a mismatch stops with both
digests printed, and a release that predates the file — or a machine with no
sha256 tool — is told so and continues. This is integrity against a corrupt
download, not a signature; releases are still unsigned.

CI now runs where the code branches

The suite runs on macOS and Windows as well as Linux, and under -race on the
two images with a C toolchain cgo can find. This is what found everything in the
first section, and it will keep finding what a developer's machine cannot show.
golangci-lint runs the set named in .golangci.yml — govet, ineffassign,
misspell, staticcheck's SA checks — plus a gofmt pass over tracked files, and
it gates the push. Turning it on caught two pieces of dead code in the editor
along with the style findings.