<feed xmlns='http://www.w3.org/2005/Atom'>
<title>newos4, branch v4.6.156</title>
<subtitle>newOS4 - the browser terminal that runs WebX11 user spaces</subtitle>
<id>https://newos.webx11.duckdns.org/newos4/atom?h=v4.6.156</id>
<link rel='self' href='https://newos.webx11.duckdns.org/newos4/atom?h=v4.6.156'/>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/'/>
<updated>2026-11-07T18:29:06+00:00</updated>
<entry>
<title>fix(newos4): workspace re-opened the modules panel every tick; picker lost two cards; commander was black</title>
<updated>2026-11-07T18:29:06+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T18:29:06+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=a46ebee30cfbbac86abb45068411bc28e1c65503'/>
<id>urn:sha1:a46ebee30cfbbac86abb45068411bc28e1c65503</id>
<content type='text'>
Three defects reported against v4.6.155, each reproduced before touching code:

1. 'the modules tab keeps opening' (workspace). api.capture() runs dispatchCommand() for real, and
   the Modules card called bare 'modules' — whose only effect is terminal.openModuleManager().
   Every 15 s refresh re-opened the panel (measured: #module-manager display:flex at t+9s and
   t+18s). The card now captures 'listmods' (same inventory as text), and api.capture() refuses
   UI-only commands outright so a future card cannot reintroduce it.

2. 'the picker got reduced to 4 items'. showPicker() used this.list() once four modes were
   registered — but modes are lazy, so that is 'modes you have already visited'. It now always
   renders the six in level order, taking registered specs for label/tagline.

3. 'commander is mostly black' on a phone. The base sheet's #ui-root rule used
   body[data-ui-mode]:not(...) #ui-root (1,2,1) and therefore beat every mode's own
   body[data-ui-mode='x'] #ui-root (1,1,1): commander/studio/cockpit asked for display:grid and got
   flex column. Commander collapsed to a 253 px centre column in an 844 px page with both rails
   hidden. Base specificity is now (1,0,0) with an explicit !important hide for classic/boot, and
   the commander phone query pins grid-template-rows: minmax(0,1fr) so the single row fills.

Measured after: picker 6 cards after visiting four modes; #module-manager stays display:none
across a refresh cycle; commander display:grid, phone centre 390x810 with a 673 px terminal,
desktop 232/788/260 columns. probe-ui-modes.py --cycle -&gt; ALL PASS, 13 checks; shell test 25/25.
v4.6.156
</content>
</entry>
<entry>
<title>fix(newos4): second review round — identity for every stock node, load dedup, sweep race, a11y</title>
<updated>2026-11-07T18:03:38+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T18:03:38+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=05d72563637e88825ff852d803621c567de47e34'/>
<id>urn:sha1:05d72563637e88825ff852d803621c567de47e34</id>
<content type='text'>
Four models (kimi-k2.7-code, deepseek-v4-pro, minimax-m3, glm-5.3) reviewed the shipped change
set. Review log with every accepted and rejected finding:
.hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md

- identity guard now covers every snapshot node, not just #terminal-container: terminal.js also
  binds #output, #command-input, #input-area and #status-bar, so a cloned one leaves the terminal
  writing into a detached node while the page looks perfect
- _restoreStock anchors on the next stock node already home, then on the recorded trailing
  sibling; a raw child index was measured wrong (terminal,input,status-bar,quick-actions)
- _loading map: a double-clicked card or a repeated 'ui easy' loads the mode once
- _sweepPending returns while that id is in flight instead of declaring failure at 6 s and
  overwriting the stored preference
- _syncChrome() on every switch (quick-actions was left in the tab order behind the mode root)
- picker: role=dialog/aria-modal/labelledby/focus, shown after 600 ms instead of waiting for a
  registration that never happens with lazy modes, and suppressed when ?ui= already applied
- ?ui= must be one of the six; _loadMode only accepts packages['ui-&lt;id&gt;']; hasOwnProperty on modes
- VFS names are shell-quoted before reaching api.exec (cat/edit)
- refresh generation no longer bumps per tick (slow captures left later panes at '...')
- easy: padding shorthand reset the 34 px footer reserve (footer overlapped the composer)
- cockpit panes get tabIndex and focus; F1/F12 left to the browser; nothing swallowed in inputs
- api.el lost its unused innerHTML sink; the test lost its _parseArgs stub (it hid the regression)
- v4.6.155
</content>
</entry>
<entry>
<title>chore(newos4): v4.6.153 — cache-bust the review fixes</title>
<updated>2026-11-07T17:57:47+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:57:47+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=e0b87c7b5a902dea69ac45ffa2e525387edebf42'/>
<id>urn:sha1:e0b87c7b5a902dea69ac45ffa2e525387edebf42</id>
<content type='text'>
</content>
</entry>
<entry>
<title>fix(newos4): four-model review — lazy-load race, restore order, a11y, key handling</title>
<updated>2026-11-07T17:56:04+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:56:04+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=fc24fdbe5d5bb504a967989122a66df9f9a7c6d7'/>
<id>urn:sha1:fc24fdbe5d5bb504a967989122a66df9f9a7c6d7</id>
<content type='text'>
Reviewed the shipped change set with kimi-k2.7-code, deepseek-v4-pro, glm-5.3 and minimax-m3.
Findings below are the ones that survived verification against file:line; the rest are in
.hermes/plans/2026-11-07_163420-newos4-six-ui-modes.review-log.md

Accepted and fixed:
- apply() had no generation token: a lazy mode load that finished after the user switched to
  another mode force-applied the abandoned one (`apply(want,{force:true})` in the .then).
  Every apply() now bumps `_loadSeq` and the callback acts only while its token is newest.
  Same token also removes a double mount: registerMode() already applies the mode as it
  registers, so the callback was mounting every lazy mode twice (teardown+cleanup twice).
- _restoreStock() used the recorded `nextSibling` as insert anchor. Every real mode moves
  SEVERAL stock nodes (terminal, input, status bar), so the recorded sibling is inside the
  mode container and every insert fell through to append. Now anchors on the next stock node
  that is already back home. A raw child index also fails (elements removed shift the
  indices) — measured order terminal,input,status-bar,quick-actions.
- _abortToClassic() left the broken mode persisted and `_pending` set, so the next reload
  retried the same failure with no way out. It now clears both and stores 'classic'.
- ?ui= reached classList.add('wx11-ui-' + id) unvalidated: a space or colon throws a
  DOMException and boot dies. Validated against /^[a-z][a-z0-9-]{0,15}$/ and no longer
  persisted (the comment always claimed it was a request, not a preference).
- Modes hide .prompt-label, so #command-input had no accessible name at all. The shell sets
  aria-label once when a mode reparents it.
- cockpit: preventDefault on all F1-F8 killed help, the address bar and devtools. F1/F12 are
  left to the browser and nothing is swallowed when focus is in a text field.
- commander: Ctrl/Cmd+K opened the palette even while typing in the composer. Ignored from
  inside editable elements now.
- picker: role=dialog + aria-modal + aria-labelledby + focus on the first card.
- api.capture(): strip the quoting characters from argv (capturePipeOutput takes argv).
- stale `wx11-ui-*` body class removed on switch instead of accumulating.

Rejected with evidence:
- "loadModuleFromURL may not return a boolean" — modules/core/module-manager.js:56 returns
  true on success and :60 false in the catch. Verified by reading the function.
- "_abortToClassic does not clear _pending" — it did not; that one is accepted above. The
  sibling claim that it "leaves the broken mode persisted" was verified by reading the
  function: it never touched localStorage, so also accepted.
- "mm.LATEST_VER / webx11_cache_ver are never set, so the mode URL falls back to ?v=0" —
  modules/core/module-loader.js:12 sets window.webx11.moduleManager.LATEST_VER and :14-15
  writes webx11_cache_ver. Verified live: the mode fetch carries ?v=v4.6.152.
- "moduleManager.modules may be a Map" — it is a plain object (terminal.moduleManager.modules
  .CryptoGzipModule is the documented access path), so ui-commander's Object.keys works.

Verified: node modules/test/test-ui-shell.js -&gt; 25/25 (the test now moves TWO stock nodes; a
raw-index restore fails it with "terminal-container,input-area,status-bar,quick-actions");
probe-ui-modes.py --cycle against production -&gt; ALL PASS, 13 checks.
</content>
</entry>
<entry>
<title>docs(newos4): rebuild changelog for v4.6.152</title>
<updated>2026-11-07T17:29:03+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:29:03+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=e894b666fd9d51f41fd9f9c7a2a3e0cf4fef2511'/>
<id>urn:sha1:e894b666fd9d51f41fd9f9c7a2a3e0cf4fef2511</id>
<content type='text'>
</content>
</entry>
<entry>
<title>fix(newos4): mode layout constraints, capture() arg split, suppress classic welcome modal</title>
<updated>2026-11-07T17:28:29+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:28:29+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=9d16b23a6a64af04a25c6ca950bf6a1cf0ccf07c'/>
<id>urn:sha1:9d16b23a6a64af04a25c6ca950bf6a1cf0ccf07c</id>
<content type='text'>
- #ui-root is a flex column (it was display:block, so every mode's flex:1 was inert and the
  terminal collapsed to 0px while the composer sat at the bottom of a 4600px flow)
- overflow:clip on #ui-root and body: overflow:hidden is still a scroll container, and the
  terminal's scrollIntoView() on new output scrolled the mode's own chrome off-screen
- terminal min-height 100px: in the tabbed/panel modes its grid parent is not a flex parent
- api.capture() split arguments itself; term._parseArgs does not exist, so every panel showed
  'unavailable: term._parseArgs is not a function'
- the classic Let's Get Started modal is suppressed in non-classic modes: it covered easy's cards
</content>
</entry>
<entry>
<title>docs(newos4): v4.6.152 changelog + change-history row for the six modes</title>
<updated>2026-11-07T17:20:27+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:20:27+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=c2f5c87529376c9f0a5d577378505489de8c85b8'/>
<id>urn:sha1:c2f5c87529376c9f0a5d577378505489de8c85b8</id>
<content type='text'>
</content>
</entry>
<entry>
<title>feat(newos4): six interface modes (easy -&gt; cockpit) over one terminal</title>
<updated>2026-11-07T17:19:47+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T17:19:47+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=aac2b1c0689aefeb140e08c24278ec486611fe10'/>
<id>urn:sha1:aac2b1c0689aefeb140e08c24278ec486611fe10</id>
<content type='text'>
Symptom: newOS4 had exactly one layout. A first-time user had no way in that did
not require learning commands, and an operator had no surface showing live runtime
state; the only way to see heap/VFS/tool-call telemetry was to type cat /sys/...

Root cause: no UI layer existed. main.html hard-codes one flex column, and
terminal.js binds the stock nodes by id at construction, so an alternative layout
had to either duplicate the output stream or reparent the real nodes.

Fix:
- modules/core/module-uishell.js (new): mode registry, #ui-root, stock-node
  snapshot/restore, identity check that reverts to classic when a mode removes or
  clones #terminal-container, shell-docked #ui-foot (the AI stop button exists in
  every mode), ui command, gated first-run picker, ?ui= honoured only if the mode
  actually registers, lazy mode loading via the same loader ext modules use.
- modules/ext/ui-{easy,chat,studio,workspace,commander,cockpit}.js (new): six
  reparenting skins with scoped CSS. Workspace/commander/cockpit read real /sys/*
  mounts and captured command output; no value on screen is invented.
- modules/test/test-ui-shell.js (new): 25 assertions - teardown, clone rejection,
  failed-mount fallback, footer docking, destructive-mode rejection.
- terminal.js: register the shell; make one failing ext module stop aborting the
  rest of the serial load loop (it did, silently, which is why six ui-* files at
  the end of extKeys never loaded).
- module-loader.js: LATEST_VER v4.6.152. The six ui-* keys are deliberately NOT
  in extKeys - see the rate-limit note below.
- admin/scripts/probe-ui-modes.py (new): 6 modes x 2 viewports plus a --cycle pass
  that switches all seven states on ONE page and asserts the output node keeps its
  identity.

Also fixed, found while testing (pre-existing, production-affecting):
- nginx limit_req zone=webx11 burst 50 -&gt; 200. One newOS4 boot fans out ~40 assets;
  the bucket emptied mid-boot and every later module got 503. Measured in
  webx11.error.log: "limiting requests, excess: 50.696 by zone webx11" against
  modules/ext/ui-chat.js. A reload within ~50 s silently broke modules. Sustained
  rate unchanged at 60r/m.

Verified: node modules/test/test-ui-shell.js -&gt; 25/25; removing both _restoreStock()
calls measures 20/25 with five FAILs; probe-ui-modes.py --cycle against production -&gt;
ALL PASS, 13 checks; served bytes: v4.6.152 x3 in main.html, class UIShellModule x1
in core-bundle.js, six ui-* packages in packages.json.
</content>
</entry>
<entry>
<title>fix(newos4): document curl in help command table (auto)</title>
<updated>2026-11-07T07:32:28+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-07T07:32:28+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=a64aae243cb95f021f21af791f29645f67a098d1'/>
<id>urn:sha1:a64aae243cb95f021f21af791f29645f67a098d1</id>
<content type='text'>
Plan: .hermes/plans/2026-11-07_061825-newos4-curl-help-row.md (T1)
Root cause: curl shipped in v4.6.151 (cryptogzip.js:19 getHelpInfo,
:612 commands['curl']) but help.html's command table was never updated,
so check_help_coverage() reported 365/366.

Change: one &lt;tr&gt; row added after the wget row in help.html's command
table. No JS/PHP touched; cryptogzip.js, modules-index.json and atlas
unchanged as specified in T2.

Verification:
  grep -c '&lt;code&gt;curl ' help.html          -&gt; 1
  wget row at 1086, curl row at 1087       -&gt; adjacent
  grep -c "_isBase64" modules/ext/cryptogzip.js -&gt; 0 (unchanged)
  git diff --stat                          -&gt; help.html | 1 +
  python3 scripts/newos4-maintain.py       -&gt; PASS: help coverage 366/366,
                                              maintenance result: OK,
                                              needs OK: none
  HTTP help.html                           -&gt; 200
</content>
</entry>
<entry>
<title>release: v4.6.151 — wget that actually saves, web_fetch that actually pages</title>
<updated>2026-11-06T15:19:59+00:00</updated>
<author>
<name>Agando</name>
<email>roduccomm@gmail.com</email>
</author>
<published>2026-11-06T15:19:59+00:00</published>
<link rel='alternate' type='text/html' href='https://newos.webx11.duckdns.org/newos4/commit/?id=0cb252dfbeb02d23cd92bad3ca8df76be22d9f92'/>
<id>urn:sha1:0cb252dfbeb02d23cd92bad3ca8df76be22d9f92</id>
<content type='text'>
S1 (reported by agent-os4-maint-01, wrongly refuted by the chat auto-reply):
wget existed but was unusable and dishonest.
- Cause 1: _cmdWget used the page's own fetch(url), so every cross-origin URL
  died on CORS ("Failed to fetch"). Measured in the served v4.6.150 page:
  fetch(cgit README) threw, /api/fetch returned 200/1954 bytes.
- Cause 2: _vfsWriteBytes ignored writeFile()'s boolean, so the command printed
  a green tick when the target directory did not exist. Measured:
  fs.writeFile('nosuchdir/x.txt') === false.
- Cause 3: bytes were round-tripped through String.fromCharCode, mangling UTF-8.
- Fix: fetch through /api/fetch (raw source), mkdir -p, writeFile + read-back
  verification, error instead of a tick on failure. Register curl with
  -s/-o parsing delegating to the same writer so the two cannot drift.

S2 (same reporter): web_fetch could not page and said it could.
- Cause: the handler honoured max_chars up to 50 000, then _clipToolResult kept
  toolResultChars-200 = 11 800, so 38 200 chars vanished silently; offset was
  not in the schema and was never read, so next_offset pointed at a page the
  caller could not request again.
- Fix: offset/next_offset/total_chars/source_bytes in the contract; the page is
  built, measured against the clip budget and shrunk until the envelope fits,
  so next_offset - returned_chars is always 0.
- Also fixed: _clipToolResult appended "0 of N chars withheld" when the content
  already fitted, claiming a loss that did not happen.

C1: fetch &lt;url&gt; --save=FILE — the only terminal path from a remote URL to the
VFS, so a 90 KB source page no longer has to be read through 3 000-char
scrollback.

Proxy: /api/extract now returns source_bytes (raw page size) and
content_chars_utf16. Python len() counts code points while JS String.length
counts UTF-16 units; 7 astral characters in the changelog left a 7-char gap at
the end of a 90 KB page (measured). _html_ok declares charset=utf-8.

Verified: node --check on all four modules; py_compile; bundles rebuilt;
served bytes identical to disk for all five assets at ?v=v4.6.151; live in the
browser: curl -s -o review/main.md &lt;cross-origin&gt; saved 1954 chars with the
correct head, a 404 wrote no file, fetch --save wrote 91 518 chars,
web_fetch p1 returned 11 754 with next_offset 11 754 (gap 0), page 2 started at
a different offset with next 23 496.
</content>
</entry>
</feed>
