A new install has no seen list, so every new OpenCode user was still warned about
an October incident that is long over. The mechanism stays for the next notice;
an empty list is now a valid file, and the validators keep running over whatever
it holds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Classify OpenSSL and LibreSSL's SSL_connect errors as unreachable again (`_` is a
word character, so \bssl\b missed them and the user read the temp directory as the
reason), point a cases-folder refusal at Clone Repo instead of Create New, and move
the mechanism and history out of CLAUDE.md into architecture-invariants.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
selectSession() toggles .active at once while _syncTabStripCorner() only runs in
the debounced render, so a cap set after measuring left a newly selected first
tab wider than the room and dropped the first row below the toolbar until then.
max-width changes nothing on a narrower tab, so the cap is simply always on, and
the clear-before-measure and finish-the-animation loops go. The no-usable-room
test uses 8 tabs: at 40 the scroll fallback fires first and it passed at any
threshold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-four review smaller item: one getCustomModelForPersist().launchModel expectation, like the pi/grok/omp tests beside it, pins #750 on the one-shot path.
Round-four review item 4: focusing the search input on every picker open raises the OS keyboard over the list on phones; guard it the way readmymind-ui.js does.
Round-four review item 3: the swap route wrote the new config.toml into the very dir a live codex session uses as CODEX_HOME before checking the pattern, and its refusal then deleted that dir. buildCustomModelInjection is pure, so the check runs on its result and a refused id never touches the filesystem. Pinned by a two-apply codex route test.
- share one context-length resolution (discovered figure, endpoint default fallback) across the routes and boot recovery, with a recovery test
- record listed per-slot context figures with a slot count of 1 and force the slot-count fallback once per rediscovery
- open up the codex model launch token for qwen3-style ids (model-plain-colon), pinned by a spawn-golden case
- pass the full discovered window to opencode as limit.context
- translate the picker search strings and document the discovery precedence
- opencode: write limit.context/output from the discovered context length (#749)
- codex: override the native launch model when a custom endpoint is selected (#750), and set model_context_window
- model picker: add a search/filter box with a no-matches line
The wrapped header strip fell back to the plain wrap whenever its first
tab was wider than the room beside the toolbar. A selected tab is wider
(its gear and close show), so selecting the first tab flipped the whole
strip back: the toolbar took its own column again and, with long names,
every tab took a line of its own. Selecting any other tab brought the
full-width rows back.
The first tab is now trimmed to the room instead (tab-corner-fit, a
max-width from --tab-corner-first-max, its name ellipsized). Only a
toolbar that leaves less than TAB_CORNER_MIN_FIRST_TAB (120px) beside
it keeps the plain wrap, which does not depend on the selection.
Two measuring details make the trim hold:
- the room comes from the rounded-up float width, less the tab's 2px
margin and a pixel for subpixel layout (a tab at the exact width still
wrapped under the float);
- the first tab is measured with its trim cleared and its gear/close
width transitions finished, or a grown room reads the last trim as the
width and a selection change is judged by the width the tab is leaving.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folder made in a cases directory is already a case (no scaffold), which Link
Existing then refuses by name and which deleting would take the linked folder
with. The route refuses a cases directory as the parent (the shared one and the
caller's own), a leading tilde in the name, and maps a vanished parent to 404.
Docs now say every name it accepts is one Link Existing accepts, and the wiki
describes the button.
git's refusal quotes the destination, so a folder named openssl or rustls
classified as HOST_UNREACHABLE and the cleanup emptied it. The cleanup now
tests git's own text, DESTINATION_EXISTS is matched first, and ssl/tls are
matched as words. Also: a test for the Docker refusal on the default branch,
the route JSDoc and constant comment, and the folder copy (empty, or new).
The name rule now matches safePathSchema (letters, digits, space and _ - . ~,
no .., no leading dot, at most 255 bytes) so every folder it makes can be
linked. A file as the parent is a 400, errno text is no longer returned,
showHidden is forwarded like browse, and Create is disabled while the request
runs. Adds multi-user scoping tests.
A direct clone whose git-slot wait timed out came back with no spawnError, so
the cleanup emptied a folder git never touched. GitRun now says whether git
started and the cleanup requires it. Cloning into a folder at or inside a
Docker case workspace is refused, as git status, fetch and worktrees do, since
git would run the new repository's hooks and filters on the host.
The picker could only choose an existing folder. POST /api/filesystem/mkdir
creates one folder inside a folder the picker may browse (same roots,
ownership and blocked-tree rules as browse), and PathPicker gains an opt-in
allowNewFolder that adds a New folder button and selects the result.
A direct clone into an empty folder emptied the folder after any failure, which
deleted a concurrent attempt's finished clone when git had written nothing.
Clones into one folder are now mutually exclusive in-process, the cleanup is
skipped when git refused the folder, never ran, or the mount is unresponsive,
and a name linked while the clone ran is a 409 that keeps the folder.
Documents path for POST /api/cases/clone.
The custom folder was treated as a parent, so the repository landed in <folder>/<case name>. The chosen folder is now the clone destination: an empty existing folder is cloned into directly (CloneOptions.intoEmptyDir, no temporary sibling, never removed on failure), a new folder is created, and a folder with files is refused.
Clone Repo could only clone under ~/codeman-cases. POST /api/cases/clone now takes an optional path (parent folder plus case name) and the Clone tab gets a Clone into a custom folder option with a Browse button, mirroring Create in a custom folder. The path is judged by prepareNewCasePath, the clone is registered as a linked case, the option is admin-only in multi-user mode, and an existing target folder is refused.
An unset user option makes `show-options -gv` exit 1 on newer tmux but print nothing and exit 0 on 3.2a (Ubuntu 22.04), so the disarm assertion failed there although the option was gone. Assert on the global listing instead, which omits an unset option on both.
`all` also sped up Run's mode colours, Stop's confirming state and the armed Ctrl key. The browser test now reads the transition at rest, since its 250ms hold would also pass the old 0.2s one.
Root without CAP_CHOWN (--cap-drop ALL, as Codeman's own Docker case containers run) or a single-uid user namespace makes chownSync throw, which failed all five smoke tests in harness setup instead of master's two.
#726 attaches the keyCode 229 recovery only when (pointer: coarse)
matches, which keeps a desktop dead key from arriving twice. Chromium
reports the PRIMARY pointer as fine once a mouse, a trackpad cover or a
stylus is attached, so on such an Android device the module was off
while the text still came from the soft keyboard, and the #441 dropped
last character and the #541 autocorrect duplication came back.
The gate is now a coarse pointer OR an Android user agent. Android ran
the recovery unconditionally before #726, so this only keeps that; a
desktop (fine pointer, not Android) and a touchscreen laptop stay off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setRemoteWorkingDir() records the path for authority only, and today
nothing else reads it for a remote session (loadFixPlanFromDisk's only
callers are setWorkingDir and the local watcher). The watcher now
remembers the remote mode and refuses both the local read and the local
watch for it, so a future caller cannot import a same-named local twin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+V's hand-built bracketed paste (#726) and the phone composer's
dropped CSI 200~/201~ markers from the text in ONE pass, so a clipboard
carrying ESC[20 ESC[201~ 1~ came out of it with an ESC[201~ that ended
the frame early: everything after it was typed as keys and each CR ran.
Both now strip until nothing matches. The composer's copy predates this
release; it is the same one-liner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#722 stopped handing a remote session's workingDir to the Ralph tracker,
which also left isFileAuthoritative false for remote sessions only. That
flag is what keeps the output-based completion guessers off, and it is
true for every local session with a working directory, so a remote loop
could end, and mark every todo done, on a prose line such as "All tasks
completed" while a local one cannot.
setRemoteWorkingDir() records the path without reading or watching the
local disk, so the fix plan stays authoritative as for a local session;
the file still reaches the tracker only through fix-plan/read, over ssh.
The test pins both, and fails on the previous head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set --rv-content-max: none on the File Viewer's rendered document, so
ASCII-diagram blocks (capped by that variable with a 720px fallback the
Response Viewer sets) follow the window like the rest of the document,
and pin it with a third assertion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Note in docs/remote-sessions.md that the @fix_plan.md auto-load does not
run for remote sessions, and rewrap the test's header comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hand the bracketed frame to the target terminal with input() so onData
scrolls to the bottom, clears the selection, and delivers the bytes.
The paste trap no longer writes the frame itself.
Fixes#716
Co-authored-by: DDT <1786035110@qq.com>
App Settings' save handled only the multi-user admin-only 403 and the tunnel's no-password 403, so a 409 for a settings.json that does not parse, a 400, a 500 or a dropped connection (`_apiPut` answers null) still toasted "Settings saved" and closed the modal, and the run mode and respawn preset writes failed with no word at all. Those failures now warn with the server's reason (else the HTTP status, else that the server could not be reached), the modal stays open, and nothing that assumes the server stored the save runs: Apply's refresh, the tunnel-starting toast and the gesture-control reload. Since the modal now invites a second Save, the plan-usage collection flip and the gesture reload are judged against the settings the failed save started from until a save lands, so the retry still sends the flip and still reloads.
A save with the tunnel switch on is read as a tunnel refusal only on a 403, so another failure no longer turns the switch off or shows the no-password message, and an accepted no-password retry now resends the whole save instead of the tunnel keys alone, which had left every other setting unsaved behind "Settings saved". Run mode and respawn presets stay saved on this device as before and now say they were not saved on the server. Stopping the tunnel from the welcome screen or the tunnel panel no longer says "Tunnel stopped" when the server refused the write, since the tunnel is then still running.
On every skin but og the skin block outranks the phone :active backgrounds of the toolbar buttons, so Run showed no press at all and the others at most a faint tint, eased in over their 0.2s transition, so a quick tap showed almost nothing. A press now scales and brightens (darkens on the light skins) over 80ms, two properties nothing on these buttons sets at a higher specificity, and honours prefers-reduced-motion.
A browser test presses every phone bar control on all seven skins and pins the computed transform and filter, so a later rule that takes the press away again shows up in the browser suite.
Both Update-Codeman.sh and Start-Codeman.sh derive PUID:PGID from the CODEMAN_APPDATA_PATH owner and refuse a root-owned directory. The smoke harness created that directory with mkdirSync, so under a root test runner (CI or agent containers) it was owned 0:0 and the script exited at that guard before its `build --no-cache` and `down`, failing the default and --keep-volumes smoke tests with an empty build/down log. The guard is correct; the harness now chowns appdata to 99:100 (docker/README.md's own example) when running as root, matching a real deployment.
Path links and in-document links carry a placeholder href (#, #fragment) that the click delegate resolves itself, but a middle click fires auxclick, never click, so the browser followed the placeholder and opened the dashboard root in a new tab. A middle click on those links now does what a click does; real URLs keep the browser's own middle click.