Merge-time fix for #659.
- Alt+Left/Right (File Viewer back/forward) is a display-only entry in
DEFAULT_SHORTCUTS, so the Ctrl+? overlay lists it, and a row in the
help modal's Panels group, pinned in help-modal-shortcuts.test.ts.
zh-CN entry for the new label; the "Restore size" entry is dropped,
since nothing shows that string any more.
- The wiki row moves from "Sessions and tabs" to "Everything else".
- The grip comment in styles.css says only phones hide the grip;
tablets show and use it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under 600px the window stays full screen whatever Expand or a drag says, and both
controls are hidden. Stepping onto a folder-grid entry goes through the grid so its
index follows. Alt+left/right is swallowed at the ends of the list too. Adds tests
for the key handler, the grip clamp and the history/grid case, zh-CN titles and wiki lines.
A hand-maintained src/web/public/security-notices.json carries warnings
about things outside Codeman that its users should act on. The first:
OpenCode's report of unauthorized access to its legacy Console, which
may have exposed provider API keys saved there.
- Pure core in constants.js (CodemanSecurityNotices): select() keeps the
notices that apply now (inclusive local-day dates, not yet seen, https
links, malformed entries skipped), and one naming a CLI only when
window.__codemanCliAvailable[cli] === true. Unknown means not
installed: a warning about software you do not have is noise. The CLI
id is data in the JSON; no code branches on it. plan() decides the
surface, so a load shows each notice once.
- An existing install on a new version gets the notice INSIDE the
release's update toast: the highlights as before, a warning section,
and the notice's link next to "See what's new". With no notice the
update toast is the plain one it always was.
- Everyone else (a fresh install, a same-version reload) gets its own
card in the toast stack: shield, "Security notice" eyebrow, title,
message, the link, Dismiss and the ×; amber-to-red accent bar, theme
tokens only (dark and light skins), full width on phones, an
opacity/transform entrance and none under prefers-reduced-motion,
role=alert.
- A notice is recorded as seen only when the user closes it, dismisses it
or follows its link, never when it appears. Storage blocked shows
nothing. The notice's own text is textContent and carries its own
zh-CN, so i18n.js leaves it alone; the card's own copy translates.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GitHub Copilot CLI (`copilot`) becomes a run mode: Run menu, welcome
screen, cron and CLI management once it is installed, tmux-backed like
the other agent CLIs, working/idle and the model read off its footer,
launch options through `copilotConfig` (model, resume, continue, the
multi-user-clamped allowAll), Custom Model Endpoints via Copilot's BYOK
variables, MCP sync of its mcp-config.json, a status probe, and the
`codeman agent` verbs (spawn waits for its composer, read returns its
last answer).
The plan-usage quota row that earlier rounds carried was split out on
review and is not part of this merge.
Merge-time commits on top: the wider COPILOT_* clamp and the zh-CN
README rows (maintainer), the Docker sign-in docs, divider rows inside
an answer kept by `agent read`, route tests for the screen-read
last-response branch, and doc drift (Past Sessions, wheelForward JSDoc,
COPILOT_/OMP_ in the env prefix lists).
Verified before merge on Copilot CLI 1.0.98: injected keystrokes,
bracketed paste and Enter reach the TUI in tmux (github/copilot-cli#4180
does not reproduce), a shell tool call completes inside tmux (#4223 does
not reproduce), and a session driven through this branch answered over
send-and-wait.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new App Settings row had no zh-CN entry, so it read in English in a
Chinese UI, and docs/wiki/Settings-Reference.md did not list it. Adds the
label and description to i18n.js, a row in the Agents & CLIs table, and a
JSDOM test that runs the real translator over the real index.html.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Drop GH_TOKEN / GITHUB_TOKEN from env.allowedKeys. The allowlist is one global list with no
mode context, so they were settable on every session. COPILOT_GITHUB_TOKEN is already admitted
by the COPILOT_ prefix.
- Declare capabilities.mcpConfig (copilot-json, COPILOT_HOME relocation) so Settings does not
list Copilot as unsupported while the sync-only target also lists it. The sync-only row is
skipped while the run mode is enabled.
- Require a leading alphanumeric on the resume id (path-segment in the registry, matching regex in
the schema): --resume's value is optional, so `--resume --yolo` made --yolo its own flag and
bypassed the allowAll clamp. Golden cases added; "ids only" wording corrected.
- Remove the unread overlays.credStore line and document signing in inside a Docker container.
- Add the zh-CN "Run GitHub Copilot" string, pin the new declarations in tests and update the
MCP registry expectations.
A selection mixing a file the server takes with one it refuses dropped the refused one without a word, and the Compose and paste-dialog pickers never toasted even when every file was refused. Every pick, drop and paste filter now goes through one helper that counts what the classifier refused: a partial refusal toasts how many were skipped while the rest upload, a full one keeps the existing Unsupported file type toast, and a clipboard paste that falls back to its text stays quiet.
With PUT /api/settings admin-only in multi-user mode, a non-admin's page must
not run into the 403. `_settingsReadOnly()` (settings-ui.js) reads the role
from window.__codemanUser; single-user mode and admins are unchanged.
For a non-admin, App Settings Save/Apply still saves the per-device keys to
localStorage exactly as before, but sends neither the settings PUT nor the
model-config PUT, and keeps every synced key at the server's value (the next
page load would put it back anyway). Only when the user really changed a
synced setting do they get one toast that server-wide settings can only be
changed by an admin; a form baseline taken at open keeps an untouched default
that no admin ever stored from reading as a change. The background writes
(run mode, respawn presets and their load-time migration, last used case,
case order, the tunnel toggles) are not sent, and a 403 FORBIDDEN on a tunnel
enable is no longer mistaken for the no-password prompt. zh-CN for both toasts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(codex): raise codex approval, question and trust dialogs as BLOCKED
Conflict resolved against #634 in src/session.ts _probePaneWorking():
#624's assignment (a dialog on screen is never work) is kept, and #634's
trust-dialog verdict block (log the hold once, remember the reading)
follows it, before _readWatching().
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(shortcuts): rebindable previous/next session, duplicate-chord refusal, rebinds stay out of the PTY
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The toast every user sees once per release had no zh-CN entries, so a
Chinese interface showed it, button included, in English. The button
gets an entry and the frame a translateDynamic pattern that keeps the
version, the "(+N more)" count and the highlights (those come from
CHANGELOG.md and stay English). Tested through the real i18n.js in JSDOM,
plus a pin on the strings settings-ui.js builds, which the pattern
depends on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a managed worktree's folder is deleted by hand (a user in a file
manager, an agent's rm -rf), git keeps its record and lists it as
prunable. Codeman then had no way out: the dialog showed the row as
"missing" with no action, DELETE answered "No worktree named ...", and
the name could never be used again (the branch still existed, and with
another branch git refused "a missing but already registered
worktree"). Reproduced on git 2.43; git's own `git worktree remove` on
the missing path drops the record with exit 0.
Removal (only removal, never a session start) now accepts a worktree
whose folder is gone when git lists it at exactly the managed path, as
prunable or locked, and no component on the way is a symlink or a file.
The live-session and lock refusals still apply. The dialog offers
Remove... (not Open) on such a row, with a note saying what it does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A codex dialog the session now reads off its screen gets exactly what a
Claude `permission_prompt` hook gets, so every surface that already handles
that event needs no second path: `web/screen-blocked.ts` turns the session's
`blockedChanged` edge into the `blocked` wait signal, an Approvals Inbox
item, the `hook:permission_prompt` broadcast (with `source: 'screen'`), web
push and webhooks, and a run-summary row. That lights the red "needs you"
tab alert and desktop notification, the phone overview's NEEDS YOU row and
the TUI. When the dialog leaves the screen the item it opened is resolved,
and `approval:resolved` clears every client.
It is an ALERT and never a keystroke, following #610's shape for DeepSeek:
the item is `answerInTerminal`, push carries no `approvalId` (so no
Approve/Deny), and the answer route refuses it. The dialog was read from a
screen the agent writes most of, so a forged match must cost a false alert
and nothing more. The message every surface shows is built from the CLI's
registry label, never from the dialog's text. The item gets no `capture`
callback, only its frame as context, because the inbox's option parser and
staleness sweep are shaped for Claude's dialogs and the session owns this
item's lifetime. A CLI whose hooks report blocked gets nothing from the
screen, so declaring both could never alert twice.
The answer route now checks `answerInTerminal` BEFORE its hooks gate and
names the CLI: a codex session has no hooks, and "Session mode cannot have
pending approvals" would be false about the item the caller holds.
Wait signals: `blocked` is available where hooks report it OR a CLI declares
`blockedLine` (`blockedSignalAvailable()`); `stop` stays hooks-only, since
nothing on a screen says a turn ended, so `until=stop` on codex is still a
400 (now suggesting `idle, blocked or exit`). A screen-read dialog is a
level the server keeps reading, so a `GET .../wait` that asks for `blocked`
while one is up answers at once; a caller that asked for `idle` alone keeps
getting `idle`. The MCP and `codeman agent` help text say so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The worktree API needs a front door that works from the place cases are
already picked. The toolbar case picker gains a third action row,
"Git worktrees…" (the phone case sheet gets a Worktrees button next to
Create New Case; no header button anywhere), opening one dialog per case:
- the repository's worktrees, with the managed ones (.claude/worktrees)
offering Open (start another session in it) and Remove…, an inline
confirmation with "Also delete its branch" (git branch -d) whose git
refusal is shown verbatim through textContent;
- a New worktree form (name, optional branch and start point) and a
"Start sessions with" picker over every enabled, installed run mode,
built from the registry catalogue as data, never an id branch.
Launching goes through run() with options.worktree and the same temporary
_runMode swap the custom-model entries use, so every mode keeps its own
path: claude and shell create through POST /api/sessions with the
worktree as workingDir, every other CLI through quick-start's worktree
field. Tabs are named w<n>-<worktree> and the Run button's mode is left
as it was.
Close Session offers "Kill & remove worktree" for a session that runs in
a Codeman-managed worktree; the removal waits for the server to confirm
the session is gone (closeSession now resolves true/false), and a dirty
tree keeps the worktree with git's reason in a toast that opens the
dialog. zh-CN strings and toast patterns for all of it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Attach button, the Compose and paste-dialog pickers, terminal drop and the
clipboard now take documents beside images and videos: the office formats
pdf/docx/pptx/xlsx/zip and the text family txt/md/csv/tsv/json/log/yaml/yml/xml.
A document is admitted by its name's extension (the MIME a phone attaches to a
.md is anyone's guess), buffered under the same 50MB image cap, and judged on
its bytes by the same fail-closed sniffer: a magic-number signature for the
binary formats, UTF-8 with no NUL byte for text. It is saved under a sanitized
stem of its name, so Q3 report.pdf is findable as paste-<ts>-<hex>-Q3-report.pdf.
sh/py/js/html are refused by name with a clear 400. That is a scope choice, not
a security property: file-raw already serves non-media text inert, so this keeps
the server from storing what it would serve or an agent could run, and html and
svg are the ones deliberately kept out. One client classifier (_promptAttachKind)
now returns image, video or document, and the server's dispatch mirrors it: an
image MIME wins whatever the name says, a document extension takes the document
branch, and a foreign extension with no image MIME is refused only after the
image sniff fails, so nothing the image path accepted by its bytes is lost.
- isUserBoundShortcutEvent skips toggleSessionSidebar (it has its own gate) and an
entry re-captured onto its own default, so ^L still reaches the CLI.
- Re-enabling a shortcut and Reset run the duplicate check and refuse when the
chord was taken meanwhile; Ctrl/Shift+Enter and Ctrl/Cmd+V are reserved with
Alt+1-9, and the Alt-digit check no longer excludes Cmd.
- Alt+[ / Alt+] keep their Alt/Option+[ labels via displayBindings.
- Behavioural tests (vm harness) replace the source-string test.
- Docs: wiki Rebinding / Why-chords-behave-oddly, architecture-invariants note.
- Small: _shortcutActions indentation, stale Alt-block comment, typeof check
instead of Object.hasOwn, full-width parentheses in zh-CN.
ZH_CN already had `Attach: '附加'` (the tile grid's and remote discovery's
Attach buttons). #606 added a second `Attach: '添加附件'` later in the same
object literal, which won and turned every Attach into "add attachment".
The duplicate is removed; 附加 covers the phone toolbar button too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved against #605 (and #602's test mock):
- CLAUDE.md Prompt uploads: #606's rewrite is kept and #605's bounded
workspace probe sentence follows the remote (SSH) refusal.
- test/routes/session-routes.test.ts: the remote-pane-history and
bounded-path-probe mocks are kept beside #606's _resetPasteRateBuckets
import, and both PRs' paste-image tests are kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One conflict, in i18n.js SKIP_SELECTOR: master added '.tab-name' itself
(the same fix this branch made for zh-CN tab names), so master's line is
kept and this branch's duplicate comment dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session or web-tab ref in the owner tab layout may now carry
`focus: true`. "Add to Focus" in a row's menu (right-click on a web tab)
sets it; the vertical rail then draws a shortcut to that tab in a pinned,
collapsible "Focus" section at its top, while the row itself stays where
it is. A click on a shortcut selects its tab, a right-click or the menu
key offers "Remove from Focus".
Model: `TabRef.focus?: true`, parsed strictly (only `true` or absent) and
carried through normalization, every pure edit and the legacy-order
projection; `setTabFocus()` is the pure helper. TabLayoutService needed
no change and no route was added: the flag rides the existing
`PUT /api/tab-layout`. Browser: `copyRef`/`normalizeLayout` keep `focus`
the way they keep `placement` (the browser writes whole layouts, so a
dropped field would be erased on every save), `setFocus` is a named
operation through `createEditCoordinator` and replays on a 409, and
`focusRefs()` (also on `project()`) lists what the rail can draw.
The section is its own element in #tabRail above #sessionTabs, so it
works on the flat rail (no named groups) as well as the grouped one, and
shortcuts are never rows: no Alt+N badge, no tree item, no lineage
anchor, no drag target. It is a list with one tab stop and
Up/Down/Home/End between shortcuts. Collapse is per-device localStorage
under its own key. A shortcut whose session closed or whose web tab is
not open drops out; the server already prunes deleted refs. zh-CN
strings added.
A phone-only toolbar Attach button hands the agent a photo or a video with the
keyboard down. It owns its own file input, positioned over the button so iOS
anchors the picker popover to it; the picked paths land on the prompt as unsent
text, so Compose adopts them together with anything already typed and Use
terminal keyboard hands a single-line draft back the other way.
Videos take a second path on the same route: ?kind=video streams the file to
disk under MAX_PASTE_VIDEO_BYTES (1GB, env CODEMAN_MAX_PASTE_VIDEO_BYTES)
instead of buffering it, sniffs the container from the first bytes as they
arrive, drains a mislabeled body unwritten and unlinks on any failure. It draws
on its own 3/min per-(IP, session) rate bucket beside the image path's 30/min:
at 1GB each, 30/min would be 30GB of disk a minute. Without the flag the route
is byte-identical to the image-only version, same 50MB cap and same bucket.
One client classifier, _promptAttachKind (MIME first, then the name's
extension), drives every pick and paste surface: the Attach button, terminal
drop, the tile grid's drop, the Compose and paste-dialog pickers and the
clipboard handlers. A pick that lands while the tile grid owns the terminal
goes to the PTY rather than the parked main-terminal overlay.
Gesture control now moves tiles: with the tile grid open, pinch anywhere
on a tile and carry it onto another tile (they swap) or an empty cell (it
moves there); a session tab carried onto the grid joins it there. The
overlay asks tile-grid.js what is under the hand (tileAtPoint,
tileDropTargetAt) and drops through the mouse drag's own paths
(dropSessionOnTile / dropSessionOnSlot, so _reorderTiles stays the one
move path).
Detach Tiles (App Settings > Appearance, per device, OFF by default,
desktop only):
- a tile's header let go outside the browser window opens the session as
a pop-out (detachSession, which now takes an optional size and screen
position), the tile's size, near the drop point, after a 120 ms settle;
- dropped on another Codeman window's tiles it moves there: that window
docks it through the target's own drop and posts 'tile-adopted' on the
window channel, and only the window it was dragged from lets go of its
copy (a last tile goes to the welcome screen, never onto that session);
- a pop-out's title drags back onto any grid: the session is taken back,
the pop-out is asked to close and is released (stops answering
roll-calls; one a script cannot close says where the session went), and
its stale 'detached' answers are ignored for 2 s;
- a tile's menu offers "Open in a new window", and a hand that lets a tile
go outside the grid pops it out too.
With the setting off nothing changes. No server change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new Animations section (rail item, heading, Entrance Theme and Tile
Animations rows with their options, the lab row) had no zh-CN entries.
Tab names were not in SKIP_SELECTOR (the list names .session-tab-name, which
the strip does not use), so a session called "Lab", "Soft" or "New session"
rendered translated. .tab-name is skipped now, like every other user-text
surface.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- zh-CN entries for the two new rows (label and description).
- The toast test header still described the dropped drawer logging, and the
noise test typed three members nothing uses any more.
- clickNotification's closing brace had been re-indented by accident.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A "Search sessions" box at the top of the vertical tab rail narrows the
list to the tabs whose name matches (case-insensitive substring; a web
tab matches by its title). It searches every group, collapsed ones
included: while a search runs the projection draws every group open and
the header will not toggle, and the stored per-device collapse state is
left alone. Groups with no match hide, an empty result says "No sessions
match", and the flat rail (no groups) filters the same way. Escape or
the clear button empties it; leaving the vertical orientation resets it.
It is a view filter only: rows get the same tab-filtered-out class the
sidebar filter box uses, through one shared _applyTabListFilter() over
the pure CodemanTabSearch matcher in constants.js. Grouping, order,
Alt+N badges and drag are untouched, nothing is persisted or sent to
the server. The sidebar keeps matching name plus working directory.
In the grouped tree, hidden rows and the headers of emptied groups leave
the roving walk and posinset/setsize, and the tab stop moves onto a
visible item. zh-CN strings added.
Review fixes for "place new, child and web tabs into groups".
- Spawned tabs follow their parent only with the new synced setting
spawnedTabsFollowParent (App Settings > Appearance > Tabs, off by default).
TabLayoutService.sessionCreated() reads it fresh through a
childrenFollowParent dep: off, a spawned session lands after its parent and
is marked manual in the same write; on, it follows until moved by hand. The
row menu shows the placement note and "Follow parent again" only when on.
"New session" in a group menu is unaffected. zh-CN label and a
Settings-Reference row.
- applyLegacySessionRank marks a child manual only when the requested order
splits it from its parent's block. A PUT of the current order, or one that
moves a family together, leaves every stored placement alone.
- The browser never echoes a create or delete through PUT /api/session-order,
with or without groups: the layout service already re-projects the order and
broadcasts it, and syncSessionOrder adopts it.
- The placement note in the row menu is plain text described by the menu, not
a focusable menu item.
- A tab that first appears inside a collapsed group briefly marks that group's
header.
- i18n skips .tab-name, so tab names are never translated.
- Tests: legacy-order cases for an unchanged order, a family reorder and a
split; service cases for the setting and for order PUTs; the scheduled
registration test now exercises register/undo; the grouped-create test binds
port 0; one route case runs through the real layout service.
New sessions, spawned child sessions and saved web tabs now land in tab
groups instead of always starting in Ungrouped.
New session in a group:
- The group menu gains "New session" (first item). It expands a collapsed
group and runs ONE launch with the current case and run mode; a launch
already in flight is reported, not queued.
- run({ count, tabGroupId }) threads the group through every launch path
(local POST /api/sessions, remote/docker and external-CLI
/api/quick-start). The count overrides the steppers for that launch only.
- Server: POST /api/sessions and /api/quick-start accept an optional
tabGroupId (Zod, 1-100 chars; null/empty is a 400). The routes pass it to
addSession(), and TabLayoutService.sessionCreated() places the session at
the end of that group in the SAME versioned write that first records it.
The session is added to the live map inside the owner lock (with a
rollback when the write fails), so no queued layout edit can commit it
first. The group is looked up in the creating owner's own layout, so an
unknown, deleted or foreign id is ignored instead of failing the spawn. An
explicit group is a hand placement: a child placed this way is manual.
- The create response carries the committed tabLayout (only when tabGroupId
was sent), adopted before the new tab is drawn. Responses without the
field are unchanged.
Child sessions (parentSessionId / X-Codeman-Parent-Session):
- A child nobody placed follows its parent: the server's existing
normalization already puts it after the parent's rows; the browser now
draws a not-yet-stored child there too (project({ parents })) and, while
groups exist, adopts the server's order instead of echoing its own append
back through PUT /api/session-order (which pinned every child manual).
- The row menu says where a child sits (follows, placed by hand, parent
closed, parent loop) and offers "Follow parent again" only to a hand-placed
child. A child whose parent is gone stands on its own and is never
re-adopted.
Saved web tabs:
- Web tabs are layout refs like sessions; openness stays per device. A
grouped web tab shows (and takes an Alt+N slot) only while open here and
reopens where the layout keeps it. Pinned by tests; no behaviour change.
Unchanged: the flat rail and header strip (markup, HTML5 drag, append and
save on create), the tree semantics and single tab stop, sessionOrder and
Alt+N, and every request that does not send tabGroupId.
Since 1.40.0 every agent tab draws its CLI logo through the run-mode-dot
slot, and there was no way to turn that off. App Settings → Appearance →
Tabs now has "CLI Logos on Tabs" (showTabCliLogos), right after Tall Tabs.
- Modelled on tabTwoRows: a per-device display key in the server-settings
merge and an optional boolean in the .strict() SettingsUpdateSchema,
loaded and saved by the id appSettingsShowTabCliLogos. Default ON on
every device; only an explicit false turns it off (tabCliLogosEnabled).
- CSS only, no tab re-render: applyTabOrientation() and the pre-paint
script in index.html stamp html[data-tab-logos="on"|"off"] from the
same stored blob (so a reload never flashes the logos), and one rule in
styles.css hides .session-tab .tab-harness and .home-sessions-harness.
The rows space their children with flex gap, so nothing is left behind.
- A live flip resizes every agent tab with no render behind it, so
applyTabOrientation() then re-takes the strip's one-row wrap decision
(updateTabOverflowMode) and re-anchors the lines drawn from tab rects;
a header that changes height reaches the PTY through the terminal
container's ResizeObserver, as any header change does.
- Covered: the header strip, vertical rail, sidebar, grouped rail, ledger
and case clusters and phone chips (all render the same tab markup) and
the desktop home rail. Untouched: tile and split headers, the Run menus,
the welcome launchers. The phone overview rows, the command palette and
the tab action menu draw no logo. The shell's SH pill and the status dot
stay.
- zh-CN for the label and the description.
test/tab-cli-logos-setting.test.ts drives the real openAppSettings() and
saveAppSettings() in JSDOM (the saved PUT body must pass the schema), the
server-settings merge, the defaults on desktop and phone, the live stamp
and its re-measure without a re-render, the real pre-paint script (on,
off, the phone key, the catch fallback), the CSS rule's exact selectors,
and the translations.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Split the one-shot keep-open intent from the in-flight guard and consume it before the
first await, so a Save clicked while an Apply is in flight closes the modal.
- Refresh the dependent groups only when the settings PUT returned ok (_apiPut answers null
or a non-ok response instead of throwing), and also when only the webhook save failed.
- Find the 'Apply or Save' hint by a data marker, not its English text; add zh-CN strings
for the new toast and title.
- Tests run the real saveAppSettings() (Apply then Save mid-flight, a failed PUT, a webhook-only
failure, a plain Save).
- Narrow the changeset and JSDoc to MCP sync and CLI management; touch the tray comment, the
architecture note and the wiki.
Redraw (Ctrl+Shift+R and the header button) shows five literal toasts and a
size report on the main pane, a tile or the split's Pane B. None had a zh-CN
entry, including the two the final checkup's tile Redraw fix added, and
"Failed to restore terminal size" fell to the generic "Failed to" pattern,
which left English behind. They now translate, and the size report keeps its
numbers through a pattern rule. test/redraw-toast-i18n.test.ts reads the
toasts from restoreTerminalSize() itself, so a reworded one without an entry
fails.
Comments and docs that still described an older default:
- styles.css: the Tiles header button is no longer opt-in; it is on by default
on desktop and off on phones and coarse-pointer tablets.
- terminal-ui.js: the desktop branch of getDefaultSettings is no longer always
{}; what the comment needs is that it sets no copyStripMargin.
- docs/tile-grid-plan.md: the Tiles default bullet names the tablet default.
- docs/cli-registry.md: codex's footer is read in a two-row window since
codex 0.162's hint row, not from its last row.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- i18n: the spreadsheet notice's feature words (charts, drawings, macros,
pivot tables, external links) were bare, case-insensitive zh-CN keys, so
the page translator also renamed a charts/ or macros/ folder in the Files
panel and a case of that name in the case picker. They are now scoped
'Spreadsheet feature: <word>' keys; warningText() falls back to the plain
word when the scoped key has no translation (English, no i18n). Removing
the bare keys closes this branch's regression. The older 'models' key has
the same class of problem; it is left alone here, since adding
.case-combobox-option-label to USER_TEXT_SELECTOR would also untranslate
the picker's two action rows and still miss the title attribute.
- Spreadsheet notice bar: marked data-i18n-skip. It is written already
translated, item by item, and ends with a number format's code, which the
observer's t() over the whole line rewrote ({name}, "Codeman").
- Connection tile (Header Stats Style Tiles): applyLocalization() now repaints
the indicator, so a switch back to English no longer leaves the Chinese
value word in its data-i18n-skip span until the next keystroke or ACK.
- Tiles default: loadAppSettingsFromStorage() no longer caches the
posture-dependent showTileGridButton default, so the init merge never
persists it; a 2-in-1 first opened as a tablet gets the button once docked.
Every reader still resolves the absent key through a fresh
getDefaultSettings(), so phones stay OFF and desktops ON.
- Tile grid over a split: closeSplitPane() skips Pane A's closing resize only
when Pane A becomes a tile. With mergeSplit false (Open group as tiles, a
stored grid) a Pane A left out of the set gets its full width back before
the main terminal parks, instead of keeping the split's half width.
- By-case tab strip on tablets and phones: with the boxes dissolved, the
-<case> part of a generated name shows again, so w1-alpha and w1-beta no
longer both read "w1".
Each new assertion fails against the previous source.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Case picker: the "New or link a case…" and "Case settings…" rows that
replaced the translated + and gear buttons get zh-CN entries reusing the
buttons' wording, plus the list's "No cases match".
- App Settings, Bottom bar: the whole group translates as one (heading, the
four Git status rows with #543's max repositories and git timeout, and
their descriptions), so it never reads half English. The Git panel's two
button names, which the new "Git status" key reaches, read naturally too.
- Toasts: the three "Could not open a new window for this ..." errors and
the dictation-closed warning.
- Spreadsheet preview: "Spreadsheet preview failed (<status>)" gets a
pattern. Worker refusals map their error code to one user sentence each in
the renderer (the raw message goes to the console), feature ids read as
words (pivot tables, external links), and the notice bar translates each
item before the join; a number format's code passes through untouched.
The worker and core are unchanged, so their pinned strings and the
asset version hash stay as they were.
- Header Stats Style Tiles: the connection tile's value word reads in
Chinese through scoped "Connection tile: <word>" keys, never bare-word
keys ("retry" is also the orchestrator's Retry button, "LIVE" a resume
list badge). The indicator cache now includes the UI language, so a
language switch repaints it on the next update.
- Tile grid: the "Loading…" label was CSS content text the translator
cannot reach; it is now content: attr(data-loading-label), written through
the translator when the tile is built and each time it starts loading.
Tests pin every new string (zh differs, no English left, English unchanged)
and fail without these changes: the tile harvest sees the loading label and
the rename field, a CSS guard keeps words out of tile generated content, the
Bottom bar group, the case picker rows and toasts read from their source,
the connection tile across a language switch, and the spreadsheet error
codes, notice bar and status line.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A file dragged onto the tile grid navigated the browser away: the single
view's drop handler sits on #terminalContainer, hidden while tiles are
open. The #tileGrid section now cancels every file dragover and drop
(bubble phase, so tab and tile drags stay with _acceptTabDrops), and a
drop on a tile uploads its images to THAT tile's session through
_uploadAndInsertImages, with the same "Only image files are supported"
toast as image-input.js (now in the zh-CN table).
- App-driven refocus no longer moves DOM focus into another session's
xterm: a remote delete of the focused tile, _reconcileTileGrid and a
socket closed with 4003/4004/4010 (_onTileExit) pass focus: false.
removeTile gains a focus option; user-initiated removes keep focusing.
- Popping out the last tile left the parked terminal's stale content under
the popped-out tab (and snapshotted it on the next switch).
_selectAfterTileGrid treats a detached session as unusable for both the
focused id and the fallback.
- "Open group as tiles" and Ctrl/Cmd+click with the grid closed pass
mergeSplit: false, so an open split no longer adds its two sessions on
top of a set already sized to the group, the count and the window.
- Touch-primary devices (primary pointer coarse: iPad, Android tablets)
default the Tiles button OFF in getDefaultSettings(); touchscreen
laptops (fine primary pointer) keep the desktop default ON. The button,
the App Settings chip and the Ctrl+Shift+G gate all resolve an absent key
through these defaults, so they agree. CLAUDE.md and the invariants doc
say so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Skip zero-size spreadsheet cells in renderTile. On sheets past the
8,000,000 px scroll cap, a cell clipped to nothing at the spacer's edge
(or a visible row or column the worker clamped to 0 px) was still created,
and the cell padding and border drew it as a 5 px box below the spacer
that grew the scroll area. The size is now computed before the element is
created and such cells are skipped, the same way the heading loops already
skip 0 px rows and columns. spreadsheet-preview.js is not an input of
SPREADSHEET_ASSET_VERSION, so the asset token stays valid.
Add zh-CN entries for the static spreadsheet preview strings (loading,
too large, no visible worksheets, empty worksheet, the warnings label,
timeout, failure, the four parser start and message failures, and the
unavailable message from panels-ui). The file-preview body is not a
skipped surface, so the exact-match entries apply with no code change.
The worker's admission refusal messages and the dynamic status message
stay English for a follow-up.
docs/security-architecture.md described the attachment gate as a
6-extension allowlist; it now names SUPPORTED_ATTACHMENT_EXTENSIONS in
src/attachment-registry.ts and what it covers, including the xlsx this
PR adds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
App Settings now refuses a Default Codex model that the server would reject,
before anything is written to localStorage. SettingsUpdateSchema is .strict()
and checks codexModel with ^[a-zA-Z0-9._\-/]*$, so a value like gpt-oss:20b
400'd the whole settings PUT while the toast still said "Settings saved", and
because the bad value was already in the local blob every later save from that
device failed the same way. The client check uses the same pattern, shows an
error toast, focuses the field and keeps the modal open. The toast has a zh-CN
translation in i18n.js.
src/web/codex-launch-defaults.ts gets an @fileoverview (fill only unset fields,
re-validate persisted values, callers decide scope, never writes Codex config
files), as every module in src carries one.
Both new Codex rows in index.html carry has-field, like every other App
Settings field row, so on phones the input and the select stack under their
label instead of squeezing it into a narrow column.
The Agent CLIs wiki paragraph said the defaults apply to every local launch.
Scheduled (cron) codex jobs are built without a codexConfig and never get
them, while Resume goes through POST /api/sessions and does, so the sentence
now names the Run menu, Resume, POST /api/sessions and /api/quick-start, and
says cron jobs do not use them.
The Settings Reference lists the two new rows in the Agents & CLIs table. The
neighbouring "Bypass approvals and sandbox" row described Pi's project trust;
it is the Codex --dangerously-bypass-approvals-and-sandbox toggle, so its note
says that now.
The PR's own changeset is removed: the release writes one consolidated
changeset at COM, and the PR's text overstated the scope (it included cron).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The welcome overview drew every CLI as its own gradient pill (one colour,
gradient and glow per CLI, plus skin overrides that re-tinted three of them),
all at the same size with the same generic play icon, wrapping into an
unaligned cluster with Cloudflare Tunnel styled as one more launcher. Nothing
said which action mattered.
Now the screen has one obvious thing to press:
- A single primary button for the first AGENT in the registry catalog (Claude
Code on a stock install), in the accent fill the toolbar's Run button uses,
with the CLI's real logo on a small light disc (a brand mark straight on the
accent turns muddy) and the same translatable "Run <label>" text.
- Every other enabled CLI as a slim, uniform pill under it, in the Compact
header's language: control-bg surface, control-border, small type, the
logo from the shared run-mode-dot slot, the bare name, and "Run <label>" as
tooltip and accessible name. Catalog order, centred, wrapping when needed.
- Cloudflare Tunnel as a quiet text link under the launchers (same id,
handler and cloudflared gate; text colour carries the active and connecting
states), still directly above the QR it reveals.
The primary is chosen by catalog order and kind, never by an id, so with
Claude disabled the next agent takes the slot and a custom CLI listed first
gets it like any other. The buttons carry no per-CLI class any more: the id
travels only as data-mode and the logo slot, and all the per-CLI welcome
gradients and their skin overrides are gone. Everything is token-driven, so
it follows every skin; OG gets its own toolbar Run blue with light text
because its --accent-ink is an unused placeholder. Focus rings sit offset
from the fills, hover motion is off under prefers-reduced-motion, and the
phone rules (reached only with the phone overview off) span the primary and
keep the chips wrapping.
Tests: run-mode-ui pins catalog order across primary and chips, the
kind-based primary choice (Claude off, shell listed first, custom agent
first, shell only), the logo slot and absence of id classes, the token-only
CSS with no .welcome-btn rule left, and the tunnel link's place between the
launchers and the QR.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner's picks after testing the 1.36.0 beta:
- Header Stats Style defaults to Compact (two pills with rings) instead of
Tiles. The resolver, the pre-paint stamp and the App Settings option all
agree; an unknown value now reads as compact.
- The Tiles header button is ON by default everywhere but handhelds (their
defaults object keeps it off, and the button still needs a 1180px window).
The Ctrl+Shift+G gate in tileShortcutFor() now resolves an absent key
through the device defaults too, so the chord and the button cannot
disagree; before, it required a stored true.
- Tab Layout defaults to Classic (the single strip, as before). By state, By
case and Ledger stay available as opt-ins. The tile-grid beta the owner used
last had only this layout, and it is the one they wanted back.
zh-CN option labels follow the new "(default)" markers; docs and wiki updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#538 added three App Settings rows (Tab Layout, State Order, Header Stats
Style) with their descriptions and nine options, and none of them had a
zh-CN entry, while #561 in the same release translated every string it
put on screen. With the language set to Chinese those rows stayed English
in the middle of a translated settings page.
Adds the 15 entries next to the other tab-bar settings. The header
style's "Tiles" is 磁贴, not 平铺: 平铺 is #561's word for the tile grid
(the Tiles button and its setting), and "Tiles (default)" must not read as
the grid. The test runs the real index.html through the real translator
in JSDOM, checks every label, description and option of the three rows
(the shared `desktop` tag aside), that English is unchanged, that each key
is in the dictionary once, and that the strip's state-row headings
translate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>