Repository navigation
Releases: JWE24-code/moqi
Release list
Moqi 0.5.1
Installs that work — on DeepSeek Harness 0.2.1, on Windows, on profiles made
before 0.2.1, and with a clear answer when something is missing — plus a fix
that stops a backslash from hiding a control character from permission rules.
Fixed
- Install without pnpm always failed, now it says why. The profile depends
on the app through alink:path, which npm rejects
(EUNSUPPORTEDPROTOCOL), so the installer's npm fallback never worked: a
machine without pnpm ended with a broken profile. The installer now stops
with a message naming pnpm and how to install it. - Windows. The installer found
dshthrough/bin/sh, which Windows does
not have, so no Harness packages were linked and the app could not boot. It
now searchesPATHwithPATHEXTitself, finds dsh's package next to npm's
dsh.cmdshim, and links with junctions, which need neither Developer Mode
nor an elevated shell. Themoqilauncher runs dsh's own entry script with
Node instead of spawningdsh.cmd, which Node refuses without a shell. moqi installrepairs profiles made before dsh 0.2.1. Their
pnpm-workspace.yamllackedautoInstallPeers: false, so pnpm put plugins'
@deepseek-ai/*peers into the profile, where they shadowed the Harness's
own and could stop dsh from starting ("required plugin did not activate").
The installer now adds or corrects that one line, keeping the rest of the
file, and deletes the stale copies together withpnpm-lock.yamlso the
install re-resolves without them. It prints a line for each repair, and
nothing when the profile is fine.
Security
- A backslash can no longer slip a control character past a permission
rule. The command matcher refuses any command containing control
characters, but a bash backslash directly before one consumed it unchecked,
so a rule such asbash(echo *)could auto-allow a command carrying raw
terminal escape sequences. Control characters are now refused over the whole
command, in every quoting context and in rule patterns too; commands without
control characters match exactly as before.
Changed
- pnpm is required to install the profile (
npm install -g pnpm, or
corepack enable pnpmon Node 22/24). The Harness's own plugin manager
needs it too.
Documentation
docs/install.mdand the README cover pnpm, Windows, the dsh desktop app
(nodshonPATH, so moqi needs the CLI; what to do if the app stops
starting after a plugin install), upgrading a profile made before dsh 0.2.1,
Node version managers, and npm 11's "install scripts skipped" warning
(harmless).
Internal
- CI installs moqi the way a user does, on Linux, macOS and Windows: dsh
atlatest, the packed tarball,moqi install, boot to--dump-config, and
no@deepseek-ai/*copy left in the profile. A guard fails the build if a
Harness package is ever listed underdependencies. - A watch on every dsh release. Every three hours
dsh-compat.ymlchecks
npm for a new@deepseek-ai/dshonlatest/nextand tests moqi and its
three plugins against it — published and frommain— recording results in
a "dsh compatibility" issue and opening one issue per break. - SonarCloud at zero. The 55 findings 0.5 introduced are cleared:
complexity splits in the permission rules parser, the git workspace, memory,
and the app shell, each pinned by tests written against the old code first;
no behavior change. Two sequential git loops keepawaitin the loop on
purpose and say why. npm testfinds its suites.scripts/run-tests.mjsruns every
tests/*-smoke.{ts,mjs}, thenfunctional.tsandpty.ts, each in its
own process; adding a suite no longer editspackage.json.npm test -- <filter>runs a subset and--listprints the set.- Workflows are linted. CI runs actionlint on every workflow, since GitHub
only parses a scheduled or manual one when it runs — which is how the dsh
watch first shipped with a job-levelrunner.tempit refused to start.
Install: npm install -g @deepseek-ai/dsh pnpm moqi-tui@0.5.1, then moqi.
Moqi 0.5.0
Permission rules, agent memory, and session isolation — the agent asks less,
remembers across sessions, and works on a branch or worktree of its own.
Added
- Approvals remember your answer. The approval panel shows the exact
command (read from the session log, never a display summary) and offers
1allow once,2allow for this session,3always allow — for this
project or, withtab, everywhere — and4deny. A rule such as
bash(git log *)then answers the matching calls without a panel, naming
itself on the status line; the Harness still audits every decision. Rules
are kept under$DSH_HOME/moqi/permissions/, never in the repository.
Matching fails closed: a compound command must match in every part, and
substitutions, redirects (other than/dev/null), and input redirection
always ask; a rule from a sandbox escalation names the mode it may grant. /permissionslists the rules in force by scope, revokes one after a
y/n, and opens the project or global rules file in$EDITOR; a file that no
longer parses is reported and its rules are off, never guessed at.shift+tabcycles the session mode: read-only → workspace-write → plan,
using the Harness's own permission presets and plan mode. Plan keeps the
workspace-write sandbox so an approved plan can be carried out; full access
is never a step. The footer badges anything but workspace-write.- Agent memory.
memory_save,memory_readandmemory_forgetkeep
facts across sessions under$DSH_HOME/memory/(everywhere) and
$DSH_HOME/memory/projects/<project>/, one file per fact plus aMEMORY.md
index. Every session carries the index as runtime context: a memory saved
anywhere reaches every open session at its next request, an unchanged list
costs nothing, and the system prompt is never rewritten. /memoryshows the instruction files the Harness loads here beside the
memories and index files, opens any of them in$EDITOR, and forgets one
with/memory forget <name>./memory off|onswitches memory for every
session and is remembered;--no-memory(orMOQI_MEMORY=off) does it for
one run. Off hides the list and the tools; nothing is deleted.#quick-add. A one-line# noteis never sent to the model: a picker
keeps it as a project or global memory, or appends it to the project's or
the globalAGENTS.md.- Sessions work on their own branch. A new session in a git repository
starts onmoqi/<date>-<id>, in place — no copy./worktree <name>opens a
session in a separate checkout under$DSH_HOME/worktrees/, outside the
repository;/branch <name>switches the current one;/isolation off|branch|worktreeand--isolationchoose the default./closeremoves
only a branch with no commits of its own and a clean worktree, and launch
tidies leftovers nothing open is using. The footer shows the branch. - The agent knows its workspace. An isolated session gets rules written
from its real state: commit to this branch, leave the base alone, and either
"this checkout is shared — never switch, stash, reset, or rebase" or "this
is a fresh worktree — install dependencies first". kcloses a session in the Sessions list. On a row this app owns,k
closes that session, as/closedoes. A session in another process, or on a
peer, is left alone: there is no per-session control channel, and the only
lever — the owning process — would end every session it holds.
Changed
- The fleet is now Sessions. The cross-device overview is renamed from
"fleet" to Sessions:ctrl+s, and/fleetis now/sessions. The list
itself is unchanged — local sessions and peers, grouped by device. /stackis now/tiled(ctrl+t). The stacked view is the tiled view:
ctrl+ttoggles it,alt+arrowsmove focus andalt+shift+arrowsmove a
pane.ctrl+tpreviously toggled reasoner display, which remains available
as/thinking.@completion, attachments, and/forkfollow the session's own
directory, so a worktree session completes and attaches its own files.
Removed
- The ctrl+c sessions menu. The first
ctrl+cnow only arms the two-step
quit; the Sessions view is the one place every session is listed. The
redundant open-sessions picker (and itsx-to-close row action) is gone;
ctrl+nand/closestill manage tabs.
Fixed
- Approval panels were invisible in the tiled view with one session. With
the tiled view saved and a single session open, every approval panel was
dropped and the agent waited on an answer no one could see. $EDITORwith arguments. A value likeomarchy-launch-editor --inline
orcode --waitwas run as one program name and failed; it now runs as a
command line, with the file passed safely as one argument.- An approval's command came from the session on screen, not the one that
asked, so a background session's approval showed no command. $DSH_HOMEis resolved one way everywhere, the Harness's own way (blank
means~/.dsh); a blank value no longer sends sessions or rules elsewhere.- A non-interactive run touches nothing.
dsh --profile tuiwithout a
terminal refused only after starting a session — which rewrote the shared
open-sessions list and, with 0.5's isolation, switched the repository onto a
moqi/branch. It now refuses before any of that, and moqi never saves a
state it did not load.
moqi 0.4.2
Security
- The profile installer refuses names that could leave the profiles
directory. It blocked/and\but accepted.., so a profile name could
write one level up; it now takes a flat, dotless allowlist plus a
resolved-path containment check. - CI installs and child processes are hardened. Workflow installs pass
--ignore-scripts(npm is pinned; pnpm and the Harness install keep their
scripts deliberately), and the dev scripts invoke/bin/shand
/usr/bin/env gitby absolute path.
Changed
- The Sonar backlog is down to maintainability smells. Every BUG and
VULNERABILITY finding is resolved; a dead ternary, areducewith no initial
value, a redundantvoid, and a control character in the CSI regex are
corrected, and dead imports and length-index reads are gone.
moqi 0.4.1
Changed
TuiApp's state now lives in focused controllers. The turn lifecycle
moved to aTurnRunner, the open-session strip toSessionTabs, transcript
search toSearchState, sign-in to aLoginFlow, and usage plus the fleet's
peer list totui/stateandtui/fleet; confirmations and picker keys answer
to their own modules. Sessions are listed through asession-listmodule,
harness-to-surface adapters live intui-adapt, and desktop bridges in
local-platform.TuiAppis reduced to its entry points over those seams,
and the decision is recorded indocs/adr/0001-state-owning-controllers.md.
Fixed
- Session logs are decoded through one path.
readLogduplicated the zstd
detect/decompress thatdecodeLogBytesalready owned, so the two could
drift;readLognow keeps only the file concerns and delegates the decode, so
a misnamed log decodes by content rather than failing. - An unnamed picker key no longer falls through into enter. A refactor left
the picker's default navigation case directly abovecase enterwith no
break, so an arrow key both moved the selection and activated the row it had
just focused — in/JevLoop, arrow keys toggled gates and enter looked inert. - Removed an unreachable
roleconditional intextOfRecord.
Added
- Characterization tests pinning cross-session log-read limits: text before the
maxBytescut is found and text after it is not, an over-cap compressed log
is skipped while truncated plain logs still count as scanned, and scan/skip
counts hold on a mixed store. - Repository contract docs:
AGENTS.md, a seededCONTEXT.mdglossary, and the
whiteboard plus ADR map of the refactored shape.
moqi 0.4.0
Added
- Panel commands on the
tuiHostseam.ctx.tuiHost.registerPanel()lets a
plugin contribute a command panel — rows the host draws, anenteraction,
and an optional masked secret prompt — so a feature's UI ships with its
plugin instead of this app.moqi-jev-loopuses it for/JevLoop(its API key
and four gates). Command matching is now case-insensitive, so a panel
registered asJevLoopanswers to/jevloop.
Changed
shift+enteris the composer's carriage return. Enter still submits;
shift+enter(orctrl+j) inserts a newline, so a draft can span several
lines. Bracketed paste is enabled, so a multi-line paste is inserted whole
rather than sending on its first line.- The dshfind listing is documented as automatic. The repository carries
thedsh-plugintopic its marketplace indexes, so the README no longer
describes a listing step that no release needs. - A plugin panel cannot shadow the app's own commands. The palette and
runCommandnow agree: on a name collision a built-in or Harness command
wins, and a panel answers only to a name both leave free.
Fixed
- CSI-u and modifyOtherKeys key reports are decoded. Terminals such as
Ghostty and foot report modified keys asESC [ 13 ; 2 u(shift+enter) or
ESC [ 27 ; 5 ; 13 ~(ctrl+enter) rather than a bare control byte; the
decoder now names those keys instead of dropping them. ctrl+jinserts a newline, as documented. The decoder had folded both
Return (\r) and line feed (\n) intoenter, so the advertisedctrl+j
newline submitted instead. Return and line feed are now distinct./resumelists stored sessions again. Two defects left the picker
empty: the query call passed a plain object where the session-query service
takes an optionalAbortSignal(signal?.throwIfAborted()threw, and the
failure surfaced as "no earlier sessions found"), and the row mapper read
id/createdAtoff the top level of rows that carry them nested inside
header, so every row was filtered out. The mapper now reads both shapes,
and no signal is passed./treebenefits from the same fix.- A panel survives a failed action or a cancelled prompt. Enter on a panel
row used to close the panel and leave only a status line behind when the
action threw; the panel now stays open and refreshes.
0.3.0
Added
- A first-run setup list. A fresh install opens with the four questions
every new user has — interface language, color palette, provider sign-in,
voice setup — as a list whose rows open the pickers that answer them.
Closing it records that the machine has seen it; it never returns unasked. - Live preview in the pickers. Moving through
/themerepaints the whole
app in the highlighted palette, and/langre-renders the chrome in the
highlighted language;escrestores what was in force andenterkeeps
what you landed on. Browsing by arrow key is trying it on. - Tab completion for command arguments.
/theme <par<Tab>completes a
palette name (shared prefix first, then the picker), and/lang <Tab>does
the same for languages. Completion and browsing end in the same place. - Mouse on by default. The wheel scrolls the transcript and a click on a
tab in the session bar switches to it. Text selection needs shift while
reporting is on (the app's ownalt+c/ctrl+ycopy needs nothing);
--no-mouserestores plain-drag selection. Shift+click is deliberately
passed to the terminal rather than treated as an app click. - The bell covers everything you stopped waiting for. A background job
that finishes rings with a status line naming it, as does a fleet dispatch
returning from a peer — not only session turns. Jobs are announced on the
observed running-to-finished transition, so jobs that were already done
when the app started stay quiet.
Fixed
-
The interface language no longer resets on restart.
langwas written
on every save but never read back, so a chosen language lasted exactly as
long as the process did. -
moqi, the app's own palette, and the new default. A Chinese ink
painting, which is where the name comes from (墨气): Deep Ink ground, Xuan
Paper text, Ink Wash selection and borders, Slate Smoke muted layer, with
the accents kept in the same vocabulary — cinnabar warning, indigo-wash
accent, celadon/bamboo/ochre semantics — and a light variant that is the
same painting on paper. The app now starts on it; anyone with a persisted
choice keeps theirs, androse-pineremains in the table as the former
default. -
Fifteen new color palettes.
/themenow offers twenty-one palettes where it
offered five, spanning the styles the original set did not: pastels
(Catppuccin), vivid neon (Dracula, Monokai, Synthwave), cool blues
(Tokyo Night), organic greens (Everforest), editor classics (One, Material,
Tomorrow), ink wash (Kanagawa), warm neutrals (Ayu), minimal paper, the
green CRT (Phosphor), and two more accessibility corners alongsidemono—
modus, a pair built to published WCAG contrast guarantees, and
contrast, saturated hues on a pure base. Published palettes use their own
light and dark variants as published; where none exists (Dracula, Monokai,
Nord) a light variant is derived from the palette's hues, as Nord's already
was.paper,phosphor,synthwave, andcontrastare this app's own. -
A plan probe for OpenAI Codex (ChatGPT). The route has had a sign-in
since/providerslanded but reported nothing in/usage; it now shows the
ChatGPT plan's tier, its 5-hour and weekly utilization, and any remaining
credits. The endpoint has no published contract — it is what the first-party
Codex client reads, reverse-engineered independently by more than one
third-party tracker, and it has already moved once fromx-codex-*response
headers — so the parser is written to the schema those trackers agree on and
refuses on any other shape. Its one real trap is handled: Codex states reset
moments in Unix seconds where every other provider states milliseconds. -
Provider-stated severity colors the plan bars. Anthropic's usage report
states per window how pressed it considers the limit (normal,warning,
critical), and that word now colors the bar instead of the local percentage
thresholds — the provider knows where the real cliff sits for the plan and
model in use, socriticalat 60% is red there where a percentage rule would
still call it roomy. An unrecognized or missing severity falls back to the
local thresholds, which remain in force for providers that report only
numbers. -
The Claude block names what the week's allowance went to. The same
response carries a per-surface breakdown of the 7-day window (Claude Code
versus chat versus the rest), shown as a note naming the surfaces that used
any of it — the fact that turns "the week is nearly spent" into a decision
about where to spend the rest. -
Plan readings are cached for a minute. Closing
/usageand reopening it
to re-check a number now answers from the earlier reading instead of
re-hitting every provider; failures are cached on the same terms, so a
provider that just refused is not hammered for a re-open. Countdowns on reset
lines stay current regardless, being drawn from the reset time at render
time. -
Per-session color themes.
/themeused to set one palette for the whole
app; it is now per session, the same way the model already is. Switching
tabs repaints in whichever theme that tab is on, a new session starts from
the theme of the one it was opened from (then diverges independently), and
a restored session brings its own theme back — falling back to the current
default if it named a theme this build no longer ships. -
Sign in to Claude Pro/Max and ChatGPT/Codex.
/providersopens a picker
over every credentialctx.authorizationknows how to obtain — a
human-guided sign-in a plain API key cannot replace. This app adds no
provider knowledge of its own: it renders whatever flows are registered,
the same way/pluginslists whatever packages compose the profile.
Mounting@deepseek-ai/dsh-llm-pi-airegisters a flow for Anthropic (Claude
Pro/Max) and OpenAI Codex (ChatGPT Plus/Pro) — the two providers it ships a
login for — from the moment it mounts. A flow's notices and questions are
rendered as a panel that owns the keyboard until the attempt settles: a
message plus a page and code to act on, or a prompt (text, a pasted secret,
or a choice of accounts) thatenteranswers andescdeclines. The page
lands on the clipboard the moment the notice does — not only onceenter
asks to open it, since the terminal is not always on the machine whose
browser can reach it — andenteron a bare notice also opens it with the
platform's own launcher (xdg-open/open/start). A device-code flow's
page and code stay on screen once its own prompt takes over, rather than
being replaced by it. Signing in authenticates the route; adding it to
/modelis still an ordinarydsh-llm-pi-aiconfig, documented in the
README. -
/usagereports what each provider's plan actually has left. A local
tally of tokens cannot answer that question: only the provider knows what a
prepaid balance is down to, how much of a 5-hour window is gone, or when
either resets. The pane now asks, using the credentials already in the
Harness credential store, and shows a block per route — DeepSeek's prepaid
balance split into granted and topped up, z.ai's plan tier with its 5-hour
and weekly credit windows and renewal price, and a Claude Pro/Max plan's
5-hour and 7-day utilization. Every probe runs concurrently and every one
resolves: a provider that is unset, down, or slow costs its own block one
line of explanation and leaves the rest of the pane intact, because the
comparison across providers is the whole point of it. The pane paints
immediately with what it already knows and fills the plan half in as answers
land. Quota bars are colored by how close the window is to its limit, not by
provider identity — a quota is a status reading, not a category. No secret is
read from a file, printed, or put in a URL: a token goes into an
Authorizationheader and nowhere else. The parsers refuse rather than
improvise, which is load-bearing: z.ai reports a window's limit in a field
calledusageand its consumption incurrentValue, so the obvious
reading of that payload would show a plan as fully spent while it was 1%
used. The z.ai parser matches those names exactly and cross-checks them
against the row's ownremaining, so a future rename surfaces as a refusal
rather than silently inverting the bars. -
DeepSeek's peak window now states its throughput cost, not only its
price. The two bite differently. Pricing is predictable and countdown-able
— off-peak is half price, so a long job can wait for it. Throughput is the
one that surprises people: DeepSeek enforces no per-account request limit and
does not reject requests for load, it holds the connection open instead, so
at peak what you experience is not an error but a turn that takes far longer
than usual. Saying so is the difference between "DeepSeek is broken" and "it
is 09:00 UTC on a Tuesday". -
Token spend is reported in the provider's own four billed buckets, so a
row splits prompt from output — priced differently everywhere, and one total
hides which way a route is expensive — and reports the share of its prompt
served from cache when there is one to report. That last is the one figure
here you can act on: a rate that collapses is usually a cache that stopped
being hit. -
/usageis a full-screen colored dashboard rather than a markdown
overlay, so it can carry real per-provider color: three rolling-window
sections (session 5h, week 7d — the shape Anthropic's Claude Pro/Max and
z.ai's GLM coding plan both rate-limit on — and a lifetime total that never
forgets), each a bar chart of every provider's share of the tokens spent in
that window, scaled to that window's own total rather than to its busiest
provider, so two close providers read as two bars close in length rather
than one ex...