Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus
libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities).
Co-authored-by: Cursor <cursoragent@cursor.com>
Wire hermit_getenv across the engine, rebrand WebUI/systemd/docs, and
add apps/desktop (Tauri 2 + React) with a DeepSeek-Harness-like UI.
Qt app/ is frozen; full libcstcore FFI is scaffolded behind --features engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
Topbar spans the full width; seat deck starts under it. Compact badge
and a seat chip on the right of the chrome. Hermit 1.0.2.
Co-authored-by: Cursor <cursoragent@cursor.com>
Install hermit-update / hermit-service from make install and bootstrap;
keep cst-agent-* as compat symlinks.
Co-authored-by: Cursor <cursoragent@cursor.com>
Compact the home parliament chip, animate crab states like the HTML kit,
ship hermit-icons.ttf (sbix) + frames, and install the font from make/
bootstrap/package paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Point readers at WHC/Hermit as canonical; add MIGRATION.md and
migrate_remote_to_hermit.sh so existing checkouts can retarget origin.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ship hermit binary (cst-agent symlink), ~/.config/hermit with legacy fallback,
Hermit color theme and mini crab frames, plus SVG→PUA font build pipeline.
Co-authored-by: Cursor <cursoragent@cursor.com>
Rewrite the homepage for day-to-day use, move paper/eval drafts under
docs/archive, and add a docs index so theory no longer leads the project.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring back Gitea CI + auto-publish; share release helpers; strengthen multi-turn
tool/job prompts with a unit contract; ccache/make help and --release bootstrap.
Co-authored-by: Cursor <cursoragent@cursor.com>
Password Realm no longer auto-enables parliament (keeps task/worker tools);
finished jobs fan out to agent notes + TUI; raise PAR_MAX/grep caps and tag truncations.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sanitize session titles, stamp CST on the topbar, fill work/sync/keys so
the sidebar never looks unfinished, drop duplicate ctx from the foot, and
tone down the scroll banner plus composer mode line.
Co-authored-by: Cursor <cursoragent@cursor.com>
Seat role already lives in the Seat Deck. Remove the lonely right-edge
chip and paint the session strip on panel ground so it no longer reads
as a raised bar with a hairline seam.
Co-authored-by: Cursor <cursoragent@cursor.com>
Appending the scanning caret after trailing newlines parked a lonely
block at the left margin. Overlay the tip character instead and let the
decode crest sweep through the wake.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace noisy glyph spam with a layered frontier: bright cyan plasma at
the tip, a traveling braille wake that ghosts the real text, a lock flash
as glyphs settle, and a scanning write-head caret.
Co-authored-by: Cursor <cursoragent@cursor.com>
Network tokens fill md_live immediately; the UI reveals md_shown at a
non-linear rate (slow at the tip, accelerates with backlog) so glitch
and text stay readable without lagging far behind a fast model.
Co-authored-by: Cursor <cursoragent@cursor.com>
leaveok on chrome panes and always redraw input after side/log motion
so the caret no longer sticks on the Seat Deck version line. Live
assistant text briefly scrambles at the tip then settles to plaintext.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the OpenCode-style inspector with a seat/fleet/ctx/work telemetry
rail, default-on for wide sessions, driven by 80ms ASCII micro-motion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cooler brand palette, compact CST-agent home with tagline and framed
composer, session topbar (model + chair chip), sidebar off by default.
Screenshot pipeline in tools/tui_shot.py; docs/screenshots for before/after.
Co-authored-by: Cursor <cursoragent@cursor.com>
Run inbound peer handling on its own thread so outbound dials cannot starve
:9100. Failed peers cool down exponentially. Also land --join (member seat)
and realm/peers.json seed discovery for Tailscale meshes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Peer dial used per-step read timeouts so a stuck handshake could block the
single-threaded daemon for tens of seconds; inbound hellos piled up unread
(CLOSE-WAIT). Use a wall-clock dial budget, drain accepts before each dial,
skip self, and only one side initiates idle polls.
Co-authored-by: Cursor <cursoragent@cursor.com>
Seats publish via parliament_note, sync pushes on write, member join without
stealing the chair, and worker tools are hard-gated when parliament is active.
Co-authored-by: Cursor <cursoragent@cursor.com>
CLI/Docker assert AEAD sync; status shows sealed channel; headless denials no longer pretend the user refused.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pidfile records instance+port; login restarts the service when the seat
changes. TUI /realm status|login|stop; prompts discourage filler chatter.
Co-authored-by: Cursor <cursoragent@cursor.com>
Prompt for Realm password when CST_REALM_PASSWORD is unset; map worker
request-gate exhaustion to a clear budget error with partial progress;
hard-skip identical tool failures after 3 tries. Add docs/parliament.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Inject shared parliament context into the agent environment, switch
prompts away from in-process workers when parliament is active, clear
login tickets on chair sync/new-project, and surface realm/chair/service
in /status. Tighten empty-reply handling and prefer-tool principles.
Co-authored-by: Cursor <cursoragent@cursor.com>
Ignore/clean host objects, add linux-headers, and include sys/time.h
so discover builds under musl. Docker chair↔member handshake now passes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Login claims the chair, writes a session ticket, and starts a background
service for discovery plus revision-based context sync. Password Realm
opens require --login (or CST_REALM_PASSWORD); --service/--service-stop
manage the resident agent.
Co-authored-by: Cursor <cursoragent@cursor.com>
Derive Realm keys from a user password (Argon2id, no private key on disk).
Login claims the sole chair with handover; peers share parliament context;
--new-project clears it. CLI peer/discover helpers and Docker alpine recipe
included; headless empty-answer retries raised for unattended runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Exchange enrolled certificates and Ed25519 challenge signatures so
same-Realm instances can authenticate each other after discovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
Advertise instances with a multicast beacon and a config-dir registry so
same-machine and LAN peers that share a Realm fingerprint can find each
other without TOFU pairing.
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce a user Realm keypair (Monocypher) that signs instance
certificates so same-domain peers can later auto-discover and connect.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pending earlier-revision jobs no longer skip completion repair; late
cancels preserve real exits; begin clears pause; partial undo keeps
snapshots; permissions and worktree failure paths are hardened.
Co-authored-by: Cursor <cursoragent@cursor.com>
Building the Windows TUI by hand -- the first local build since the CI file
went away -- stopped in src/tools/session_tools.c: `getline` was an implicit
declaration there, which this GCC treats as an error. src/session.c had a
private fgetc() shim for exactly that; the session-list scan is newer and used
the POSIX call directly, and only the Windows build notices.
The shim moves to os_compat as os_getline(): getline(3) itself on Unix, the
fgetc() loop on Windows, one prototype in the header so a caller reads the same
everywhere. src/session.c drops its copy and calls it; the scan does too (its
`ssize_t` becomes `long long`, which Windows has).
make check is green on Linux, the Windows cross build now finishes and
packages (dist/cst-agent-0.9.7-windows-x86_64.zip plus the NSIS setup), and the
static Linux tarball was rebuilt with it.
The release files are built on this machine from now on, so the workflow that
did the same steps on a runner -- TinyCC build and unit tests, the static musl
Linux tarballs, the mingw Windows zip, the Gitea release upload -- is gone.
.gitea/ held nothing else.
Everything the workflow called stays and is runnable by hand exactly as it ran
there: scripts/ci_linux_toolchain.sh is what package_linux.sh uses to pick its
musl toolchain, scripts/ci_tcc.sh builds the second compiler the test job used,
scripts/ci_windows_cross.sh and scripts/bootstrap_windows.sh are the mingw
build, and scripts/release.sh is the upload (GITEA_TOKEN=... scripts/release.sh
FILE...).
A phone's back key did not close the settings dialog. A window-level QML
Shortcut never sees the key while a modal popup is up, and a desktop run says
so plainly: with the dialog open, Back left backLayer() at "settings", while
invokeMethod(win, "stepBack") closed it on the spot and Escape -- the popup's
own CloseOnEscape -- closed it too. So the key reaches the popup and never
reaches the shortcut map, which is why the drawer (no popup) closed fine and
hid the problem on a phone.
A Keys handler on the window's root item is no help either: nothing reached
it, the focused TextField keeps the key. So the key is read in C++, on the
application object but only for this window's events -- a filter installed on
the window itself was never called once, while the same key closed the drawer
through the shortcut, hence the application object.
With a layer open, the window's own backLayer() names it and stepBack() closes
it, press and release both taken; with nothing open the key is passed on
untouched, which is what leaves the window on a phone and does nothing on a
desktop. Main.qml keeps only those two functions: the Shortcut and
backHasLayer are gone, --uitest installs the same handler main() does, and the
back checks now drive the code the phone runs instead of a shortcut only the
test had.
The last check asks the phone's own question and can end the run without the
closing line -- the activity leaving takes the process with it, and every line
is flushed as it is written.
Desktop: 106 ok, 0 FAIL. Phone: 104 ok, 0 FAIL (two clipboard checks are
skipped on Android), and "after the last back: window visible 0".
Open and Share failed on the phone for a reason that was in the harness,
not in the app: the card was where isVisible() says yes, but the transcript
clips it away -- the run's own note says it found contentY at -14 with the
card's row below the visible part -- so the click (and the tap after it)
landed on whatever was at that point instead, and the header stayed empty.
The run now stops the view following its tail (ChatView.stickBottom, which
the replay sets), scrolls the card's row into view with the numbers in a
note, and only then clicks. ChatView gets an objectName for that.
The checks also say what the card hands the engine (the name the store
keeps the file under, from the strip's own model) and what the card itself
answers -- which is what turned "it did nothing" into this answer:
note the card is out of the viewport (contentY -14, card at 433 of 615):
scrolling to it
ok Open hands the file to the system (opened report.md (test mode);
cards hold 'img-...png, art-....md', on screen, card says '')
ok Share names the file (shared report.md (test mode); card says '')
The whole device run is green now: 97 ok, 0 FAIL (it was 88/17 when this
session's work started, and every one of those was real).
The two clipboard-image checks failed on the phone for a reason that is not
a defect: Android's clipboard carries text and URIs, not images -- setImage
does nothing there -- so they are skipped on Android with a note saying so,
instead of reporting a phone that is broken because it is a phone.
Open and Share still fail on the device, and now say how far they got:
neither the synthesized mouse click nor a touch tap leaves a line on the
card's buttons, while the same click reaches the header and the sidebar.
The engine's own path is fine (openArtifact() called directly answers as it
should), and the only way for the header to stay empty is openArtifact()'s
early return for a name it cannot resolve -- so what is left to look at is
the name the card hands over (the model's `rel`) and whether the card is
inside the visible part of the scrolled transcript at that moment, since
QQuickItem::isVisible() does not account for clipping.
Device run after this: 95 ok, 2 FAIL (was 17 before this session's fixes).
A Controls ToolBar hands its children to a style-owned contentItem, and on
the NX789J that item sat 35 px below the bar itself: the header was at y 0
h 42 while its own gear was at y 56 -- drawn on top of the sidebar's first
lines, and clicked there too, so the settings dialog could not be opened
from the gear at all (eleven checks that follow it failed with it). A
desktop never showed it: same style (Basic), same numbers, gear at y 21.
A plain Rectangle has no such indirection -- its children are where it is.
Nothing else changed; the background and the border line are the same.
--uitest prints what it sees at that point now (style, window, header,
gear, safe area), which is how this was found:
note style Basic, window 1280x853, header y 0 h 42, gear centre y 21
Before: gear centre y 56, `FAIL the gear opens the settings dialog`, 17
failures on the device. After: 95 ok, 4 FAIL (Open/Share and the two
clipboard-image checks, which Android has no answer for).
The settings dialog check failed only on the phone, with no QML error and
nothing to read: this build's Qt messages never reach logcat (verified: a
plain qInfo from the app is not in the buffer). Two ways in, then:
- --uitest installs a message handler that writes Qt's own warnings into
uitest.log, next to the checks. What the phone did say, it says there.
- --uitest --shots saves window grabs at the interesting points into the
workspace. A device run cannot be handed CST_APP_SHOT (an app started
from adb gets no environment), so what a desktop run asks for with an
environment variable has to be an argument here. main.cpp takes it, so
`--uitest --workspace DIR --shots` works from adb.
What they said about the phone (NX789J, 1280x853):
ok ... the gear is reachable, centre 1191,56 of 1280x853
ok ... and openSettings() opens it when called directly
so the button is there and the handler works: the synthesized click is what
does not open the dialog. The run's own picture of that moment shows why --
the header is drawn over the sidebar, so the click lands on what is drawn
above the gear. On a desktop the same window has the header above the
sidebar and the click lands. Not fixed yet (nor the Open/Share checks on the
device, which are the other two failures that are not about the dialog).
The settings checks now go on either way, so a phone run reports the dialog
instead of eleven lines of "not there".
A tool that takes minutes -- a build, a wide search, a command nobody can
predict -- read as a window that had hung: the card said "running" and
nothing else, forever. The chip now counts from the point the card starts
running (from five seconds on, before that it is noise), which is where a
person looks when a call seems stuck; the status line and the Stop button
were already there. A card that appears already running (a replayed
transcript) starts its clock too.
--uitest: two checks for it (a card four minutes eighteen in says
"running · 4:18"; a finished one stops counting).
And the run now writes uitest.log in its workspace, every line and the
verdict. That is the only way to read a run *on a phone*: stdout reaches
nobody there, an app started from adb cannot be handed CST_APP_SHOT or any
other environment (so the shots a desktop run can take are out), and logcat
does not carry this build's Qt messages at all -- checked: a plain
`qInfo` from the app is not in the buffer.
First device run (NX789J, 105 checks): 88 ok, 17 FAIL, every failure
pre-existing and unrelated to this work -- the settings dialog does not
open from the gear (so nothing in it is reachable: the five dialog checks,
the two provider-form ones and the four clicks that follow), Open/Share
leave no status line on the device, and the clipboard-image checks have no
clipboard. All the md_to_pdf and card checks pass on the phone, including
"black ink on paper (darkest pixel 0)" from 4c69ef3.
The hook added in da04913 asked for getStringArrayListExtra, but `am
start --esa applicationArguments "--uitest,--workspace,/sdcard/x"`
stores a String[] -- that call answers null for an array, so the window
still started with no arguments at all. Ask for the array first, then
the list (Qt's own loader, putStringArrayListExtra), then the single
comma-separated string.
With this the uitest runs from adb on the NX789J: the fixtures and
selftest.pdf land in --workspace, which is how a device round trip is
possible without touch injection (adb cannot tap this window).
A QTextDocument takes its default text colour from the application
palette, and markdown names no colour at all: in a window running a dark
palette -- which is what a phone in dark mode hands Qt -- every plain
paragraph was laid out #eeeeee on a white page. Printed, that is a blank
document; on screen it reads as "the font is oddly faint". The first
PDFs off the phone were exactly that.
- PdfTools: paperDocument() sets the default stylesheet (body colour
#000000) before setHtml, and writePdf() lays that out. A colour the
document does name still wins, so an .html handed to the tool keeps its
own -- checked with a #cc0000 document under both palettes.
- --uitest: draws that document on a white image under a dark palette and
looks for ink (238 before the fix, 0 after).
Verified on the NX789J: md_to_pdf on the device now writes CID TrueType
NotoSansCJKHK-Regular/-Bold text whose page renders at min=0.
`am start -n <pkg>/org.qtproject.qt.android.bindings.QtActivity --esa
applicationArguments "--workspace,/sdcard/x"` left the window on its default
workspace on the NX789J (the plain-string extra too), so the extra is read
from the intent directly. That is what makes `cst-agent-app --uitest
--workspace DIR` reachable on a device with no keyboard -- and adb cannot
inject touch into this window either (tap, touchscreen tap, mouse tap and
motionevent all do nothing; key events do work), so it is the only way in.
Everything degrades to "no arguments": an invalid object, a missing method or
a context that is not an activity all come back empty.
needs_confirm() (src/agent_loop.c) decides whether a tool asks by reading
cfg->confirm_shell / confirm_write / confirm_net; the approval mode only
relaxes those. The CLI fills them in (main.c: parse_confirm), the TUI and the
WebUI do too -- the Qt window never did: AgentCore::open() memset the whole
config and cst_agent_create() keeps what it is given, so every tool ran
without asking while the sidebar said "ask". That is why nothing was ever
asked on the phone (or on a desktop).
- AgentCore: one mapping, applyApproval(), used by open() and setApproval();
the mode is remembered like the TUI's /approval and read back on start
(else "ask", the only mode where the window's consent dialog is reached).
- --uitest: two checks that the mode reaches the engine as those flags.
Version 0.9.7 (local).
The SAF picker is not 'still to come' any more (v0.9.6 did it): the list keeps
the back gesture and the icon glyphs. In its place, what v0.9.7 changed on the
phone -- a consent question refused in the background and bounded in the
foreground, the bounded GUI hop, the phase in the header, and the window's own
md_to_pdf.