Commit 42019ba
authored
vDevice, the built-in browser & DevTools, and the extension surface (#59)
* vDevice: its own keyboard, its own permission prompt, real insets and a working app bar
Four things a device needs before an app running on it behaves like an app
running on a phone — and before an agent can drive one.
**A keyboard of its own.** The device borrowed the phone's IME, which is JCode's
chrome: `screencap` of the device did not show it, `uiautomator dump` did not
list it, and `input tap` could not reach a key, so an agent could see a text
field and had no way to answer it. `tools/vdevice-keyboard` is a real APK hosted
as a container child. The seam is `View.onCreateInputConnection`, not
`InputMethodManager` — `showSoftInput` returns false before it reaches a binder
for a windowless hierarchy — so the keyboard is handed the same `InputConnection`
a real IME gets and the editing is the platform's. Text arrives as text, accents
and emoji included, instead of through `KeyCharacterMap`, which has no key for
most of it. Adds `adb shell ime show|hide|toggle|status|list`.
**A permission prompt on the device.** It was a Compose `AlertDialog` in the tab:
out of `:guest` over the binder, onto the screen, and back with the answer. Both
halves were in `:guest` the whole time, so the question never needed to leave —
and leaving was what put it in the wrong window. `VirtualPermissionDialog` is a
container child with declared ids, so it photographs and taps like everything
else. `onPermissionRequest` and `permissionResult` are deleted.
**Insets that describe this device.** A window that stops where the bar starts
was enough for an app that lays out inside its window, and nothing at all for one
that asked to draw behind it: edge-to-edge is a bargain, and the app was keeping
its half alone. The container now substitutes its own insets at the root, so what
a guest reads is the device's chrome rather than the phone's. The bar's height is
accounted for exactly once — as a margin or as an inset, never both.
**An app bar with words in it.** `onPostCreate` was never dispatched, and it is
what sets `mTitleReady`; without it `onTitleChanged` never reaches the window, so
a themed action bar was built and its title stayed empty. It is also where
`AppCompatActivity` hands its delegate the same call.
`tools/hardware-fixture` gains an action bar, an options menu and a WINDOW block
reporting the insets as dispatched, so all of this is checkable at a glance. Its
README's build recipe was missing the resource step.
Verified on an Odin2: app bar renders with its title and overflow popup; the
three status-bar modes give content heights 559 / 559 / 609, the bar's 50px
counted once; the prompt answers to `input tap` and the callback arrives; the
keyboard reads the field's EditorInfo (Web address, `/`, `.com`, Go) and types
through its connection.
* vDevice: paint the home screen after an app is stopped
Stopping an app left the device showing its wallpaper *colour* and nothing else
— no wallpaper, no status bar, no icons, not even the "No app installed"
placeholder — until the tab was closed and reopened. Taps still launched the
icons nobody could see, which is what gave it away: the launcher's data was
there, and only the paint was missing.
The tab rebuilds the surface view as a guest's screen goes, because a released
child `SurfacePackage` otherwise leaves the app's last frame on a layer over
everything the container draws. So the launcher is handed to a view created
moments earlier, whose surface does not exist yet — normally nothing to worry
about, because `surfaceCreated` paints again from the list the view is already
holding.
Except a `SurfaceView` creates its surface from an `OnPreDrawListener`, so it
needs the view tree to *draw* once after it is attached — and an app stopping is
the exact moment the screen goes still. Nothing animates, nothing invalidates,
no traversal runs, and the callback that would have rescued it never comes.
Measured: `surfaceCreated` had still not fired forty seconds later.
So the draw is asked for rather than waited for. One `invalidate()` when the
surface is not there yet; the surface arrives 17ms later and the existing
callback paints it.
Verified on an Odin2 over three consecutive launch/stop cycles.
* vDevice: radios on the status bar, quick actions in the shade, and a shade you can pull anywhere
Three things that turn a strip with notification icons on it into the device's
status bar.
**Radios on the bar.** Wi-Fi, Bluetooth and the mobile radio, on the right where
a phone puts its system icons, drawn only when the device *has* that radio and
has it switched on — the same two states a phone shows and the two an app is
written to tell apart. This is the one thing the phone's own bar, sitting
directly above, cannot say for this device: its Wi-Fi is not the phone's, and
seeing that it is off is the answer to "why does this app think it is offline".
**Quick actions in the shade.** One tile per radio the device was built with,
switching it where the app is rather than three screens away in the device's
Settings. Taking an app offline and watching what it does is one of the things
this device is for, and until now it cost a round trip through Settings — by
which time the app had usually decided. The tiles and the bar's icons are built
from the same read, so the two cannot disagree.
**A shade that pulls anywhere.** It had two holes, both of which made it a
decoration on the screens that happened to be simple rather than the device's
own shade:
- A guest's dialog or popup is a *separate window*, so it took every touch on
the screen the moment one was open, and `deviceUiUnder` did not ask about the
status bar. It does now — the same argument that already put the keyboard and
the permission prompt ahead of a guest's window.
- A full-screen app made the bar `GONE`, and a view with no height receives no
touches, so taking the strip away took the shade with it. The strip is
`INVISIBLE` instead: nothing is drawn over an app that asked for the screen,
but there is still an edge to pull from, deliberately narrower than the
ordinary grab because those pixels are the app's. Pulling the shade brings
the strip back with it, and closing it takes it away again.
The shade is opaque now. A hair of an app showing through a 22dp strip is
texture; the same hair through a full pane is a notification's title competing
with whatever button the app has underneath — measured on the hardware fixture,
whose pale full-width buttons ghosted straight through "No notifications".
Verified on an Odin2: icons follow the switches; a tile toggles Wi-Fi and the
bar's icon goes with it; the shade pulls down over an open overflow popup and
over a full-screen app, and the app still gets its own touches underneath.
* vDevice: close the shade when it stops being what the screen is about
Everybody tries the same gesture on an open shade — a tap on the app behind it —
and nothing happened. The status bar's view was only as tall as the two things it
draws, so a press below them was never delivered to it at all: the container gave
it straight to the guest, and the branch that would have closed the shade could
not be reached. Anything a view is going to dismiss itself on has to be something
the view is given, so the view is now the height of the screen.
It costs nothing while the shade is shut. A press below the grab strip is refused,
the container moves on to the app underneath, and the app never knows the view is
there — checked by tapping the fixture's own button straight afterwards and
getting the permission prompt.
The dismissal is measured against the bottom of the *panel* rather than the grab
strip. Those were the same edge while the view stopped at the shade, and are not
now: against the strip, a tap on a notification's own card would have dismissed
the thing being read.
The other three ways a shade stops being what the screen is about, none of which
involve a finger:
- an activity starts on top of the one it was pulled over
- the permission prompt goes up, and the app behind is blocked on an answer
- nobody is looking at the device any more, so nothing on it has the focus —
otherwise the shade is still hanging there on the way back, over an app it was
never opened from
Verified on an Odin2: a tap outside closes it and does *not* reach the button
underneath; a tap on a quick-action tile still toggles its radio and leaves the
shade open; the app's own touches and the device keyboard are unaffected.
* Workbench: stop the header's path from ending in the title above it
The header is two lines: what is open, and where it is. The second one ended
with the same name the first had just said — "Device sandbox" over "Default
Workspace / waverepo / Device sandbox" — so the line with the least room on it
spent its last third repeating the line above, and did it in the place a long
path gets ellipsised from.
It is not only tabs. With nothing open the heading falls back to the project,
and the trail ended on the project: "waverepo" over "Default Workspace /
waverepo". Same repetition, same fix — drop the last step when it is the one
already named above.
Only the last one, and only when it matches. A project that happens to share its
name with the tab open in it is still a real step on the way there, and taking it
out would leave a path that goes somewhere else.
* vDevice keyboard: the marked keys wear symbols, not drawings
Shift, backspace and hide came out **blank**. Everything about them was right: the
icon loaded as a 55x55 VectorDrawable, the bounds were sane (43x43 inside a
143x103 key), the alpha was 255, the canvas was hardware accelerated, the view was
on screen and drawing its own background, and taps landed on it. Replacing the
glyph with a plain opaque `drawRect` did not appear either, which is what turns
"the icon is wrong" into "this canvas is not usable": `TextView.onDraw` clips the
canvas to the space it lays text out in, and what a key with no text has is a clip
with nothing in it.
So the keys wear characters instead, which is how the rest of the device already
does it — the terminal's extra-keys row writes its arrows as text. A key is a
`TextView` either way, so a mark is now laid out, centred, scaled and tinted by
the same code that already does all four for the letters, and `uiautomator dump`
reports it as `text` beside the `content-desc` that says what it means.
The font is the one JCode already carries for this. Those code points are the
obvious ones and that is not the same as being present: phones ship *subsetted*
symbol fonts, so U+21E7 and U+232B land as tofu on exactly the devices nobody
tests on, and Noto Sans Symbols 2 is what already stops a TUI's glyphs coming out
as boxes in the terminal. This app carries its own copy for the same reason it
carries its own palette — it is a separate APK and cannot read the container's
resources — and it is given only to the marked keys, so the letters stay on the
system's own sans.
U+2304 was the obvious pick for hide and draws as a thin lowercase "v", which is
not a mark to put in a row of letter keys; U+23F7 is.
The five icon drawables are gone with it.
Verified on an Odin2: shift, caps lock, backspace and hide all draw; shift lights
and locks with the right mark on it; typing and the action key still work.
* Workbench: put the built-in browser's navigation in the Command Palette
The browser had a toolbar and nothing else. That is fine while you are looking at
it, and no use at all from anywhere else — which is where the palette is reached
from, and the palette is the one surface in this app that does not care what is
on screen.
So "Open Browser" sits beside "Open Virtual Device" under Tools, and Back,
Forward and Reload/Stop are their own Browser group. The same argument the editor
commands are already here on: a command anybody can find beats a button only the
right screen has.
Each one is offered only when it would do something. Back and Forward register on
`canGoBack`/`canGoForward`, so a Back with nowhere to go is never in the list —
a command that answers a search and then does nothing is worse than not being
offered. Reload wears the state the browser's own button does, and becomes Stop
while a page is loading, because a page is either loading or it is not and the two
are never both worth showing. All four need the browser tab to be the one in
front, which is not a nicety: its controller only exists while its page is
composed.
`BuiltinBrowser` gained a no-URL `requestOpen()`. Everything that opened this
browser until now arrived holding a URL — a preview, a link in a terminal — so
"open the browser" and "go here" were the same action; the palette is the first
caller with nothing to navigate to, and sending it somewhere would throw away the
page already loaded.
Verified on an Odin2: Open Browser opens the tab and leaves its page alone; with
no history neither arrow is listed; after navigating, Back appears; running it
goes back and Forward takes its place.
* Workbench: a compact browser toolbar, and the options that had nowhere to live
The bar was three default 48dp icon buttons over a field carrying 9dp of its own
padding — a phone browser's chrome on a pane already sharing a screen with the
editor, the tab strip and the workbench header. Nearly sixty density-independent
pixels of frame around the page that is the whole point of the tab. It is about
forty now: 30dp buttons, 17dp glyphs, and the field's padding brought down to
match.
The space that bought goes to a "more" button, and what is behind it is chosen
for what a *built-in* browser is for. This one exists to look at a dev server
running on the machine it is being written on, so the list is what that job asks
for rather than what a phone browser ships:
- **Request desktop site** — the one asked for. Composed from
`WebSettings.getDefaultUserAgent` rather than written down, so the engine
version in it is the version actually rendering the page; a made-up Chrome
number is a lie a server can act on, and this browser exists to show what a
server does. Kept in `BuiltinBrowser` because the WebView is destroyed
whenever the tab is not in front, and a mode that reset itself every time you
looked at something else is not a mode anybody can compare two layouts with.
- **Reload without cache** — the one a dev server asks for by the hour. A
preview still serving last build's bundle looks exactly like a change that
did not work.
- **Clear cookies and site data** — cookies, storage and cache, then a reload,
because nothing about a cleared localStorage shows on a page that is still
the one it was drawn from.
- **Copy URL**, **Open in system browser**, **DevTools** — the ways out.
The toggle names the action rather than the state ("Request desktop site" /
"Request mobile site"): a row reading "Desktop site" with no tick beside it is a
question about which of the two it is telling you.
Verified on an Odin2: the page reports
`Mozilla/5.0 (X11; Linux x86_64) ... Chrome/101.0.4951.61 Safari/537.36` with
desktop mode on — no Android, no wv, no Mobile, and 101 is this device's real
engine version rather than an invented one.
* Workbench: a site indicator in the browser's address field, and the panel behind it
A padlock before the URL, and a panel when it is pressed.
The mark says what the *connection* is, and deliberately not what the site is: a
padlock for https, an open one in the error colour for http, a file mark for
file://, a globe for a tab with nothing in it. A favicon in that slot would be a
picture the site supplied being used as evidence about the site, which browsers
learned the hard way — a favicon of a padlock was a phishing kit's first move.
The favicon is in the panel's heading instead, beside the host, where identifying
is the whole of what it is being asked to do.
The panel answers the questions a browser is actually asked about one site. What
this connection is, in a sentence that says what it means rather than naming it.
Who says so — issued to, issued by, expires, straight off the WebView's own
certificate, which is what a padlock is shorthand for and which no amount of
green icon substitutes for. And what the site has kept: cookies, site data, and
one button that clears both for *this origin* — the overflow menu is where
"forget every site" lives, and a login being debugged is not a reason to sign out
of everything else.
Site data is counted by asking the page, not `WebStorage.getOrigins`. That API
only knows the quota-managed storage and answers "nothing" for the one storage
anybody actually uses, so the row would have said None whatever the site had kept
— a wrong answer to the question the panel was opened to ask. The clear button
clears the same two the row counts, or it would be clearing something else.
No "trackers blocked" row. Nothing here blocks any, and a row claiming otherwise
is worse than no row.
Verified on an Odin2: example.com shows secure with SSL Corporation / Oct 28
2026; duckduckgo.com shows its duck in the heading and *.duckduckgo.com from
DigiCert; an http URL shows the open padlock in red with no certificate rows;
two localStorage keys set from the console read back as "2 in localStorage" and
return to None after the clear.
* Workbench: stop the navigation drawer taking drags meant for the browser's page
Scrolling a page slid the workbench's navigation drawer open over it. The drawer's
swipe-to-open watches the whole content area, and a page is full of things that
answer a sideways drag — a carousel, a row of tabs, a map, and plain scrolling,
which is never perfectly vertical when it comes from a thumb. The drawer kept
winning gestures that were never meant for it.
The WebView now claims the gesture on ACTION_DOWN, which is what every other
embedded surface in this app already does: the editor, the terminal, the markdown
preview, both extension hosts and the virtual device's screen all say the same
line. The browser was the one that did not.
The listener returns false, so the page still handles its own events; this only
settles who *else* may take them away. The cost is the same one those five accept
— the drawer's edge swipe does not start on top of the page, and the drawer's own
button is how it opens there.
Verified on an Odin2 against duckduckgo.com/settings: a scroll with sideways
drift scrolls the page and leaves the drawer shut, a deliberate left-to-right drag
across the page leaves it shut too, and the header's button still opens it.
* Workbench: make the built-in browser follow JCode's colour scheme
It did not. Checked rather than assumed: with the device in night mode and the
workbench painting dark, `matchMedia('(prefers-color-scheme:dark)').matches`
answered **false**, and the WebView's background was hardcoded `Color.White`. So
every site that themes itself was being told the user prefers light and obliged,
and a dark workbench flashed a full-pane white rectangle on every navigation.
The attribute that decides it is **`android:isLightTheme`**, read off the theme of
the context the WebView was built with. Not the night bits of the configuration,
which is the obvious guess and is wrong — measured, with the configuration forced
to `UI_MODE_NIGHT_YES` and algorithmic darkening allowed the page still answered
false, because this app's runtime theme has a Light parent and that is what the
WebView was asking. Two one-line themes exist now for the WebView to be handed,
and the runtime theme is left alone.
It follows **JCode's** scheme rather than the phone's, which is the promise the
rest of the app makes: the workbench's own theme setting can differ from the
device's, and a browser inside a dark window should not be white because the
phone happens to be in day mode. The answer comes from
`MaterialTheme.colorScheme` — the palette actually painting the tab — so a theme
bundle changes it too, not just the light/dark switch.
Algorithmic darkening is allowed alongside it, so a site with no dark styles of
its own is darkened rather than left glaring.
Keyed on the theme, because a WebView reads this once at construction and there is
no setter: switching theme rebuilds it and re-navigates to the same URL, which is
a page that has to be re-rendered anyway.
Verified on an Odin2: `prefers-color-scheme: dark` now answers true, and
duckduckgo.com/settings — the page this was reported against — renders dark
instead of white.
* Workbench: make the browser's DevTools console readable
Every line was the same shape: one paragraph of monospace wrapping flush to the
left margin, with the level carried by nothing but a shade of text. A long error's
second line looked exactly like the next entry, telling an error from a log meant
comparing two colours against each other, and `source:line` was appended to the
message so it wrapped into the middle of the prose it was annotating.
A line now has a coloured rail, a one-character marker (✕ ! · › ‹), the message,
and its origin on the right where an annotation belongs. Errors and warnings carry
a faint tint of their own colour — tinted rather than ruled, because a divider on
every row of a log this dense reads as a table, and the two lines worth finding
are the ones that are not ordinary.
Consecutive identical messages fold into one row with a ×N badge. That is the
difference between a usable console and a wall: one load of an ordinary site put
the same Permissions-Policy warning up three times, two wrapped lines each, and
pushed everything worth reading off the top. Only *consecutive* ones, which is
what a browser does and is the honest version — two identical errors either side
of something else are two events, and folding them together would lose the
ordering that makes a log worth reading.
The list follows the newest line, which it did not: a log that has to be scrolled
to see the thing that just happened is a log nobody looks at while it is
happening. And a long press copies the message, which is the reason anybody
reaches for a console line in the first place.
Verified on an Odin2 against duckduckgo.com/settings: the repeated warning shows
once with ×2, an input/result pair reads › then ‹, a page `console.log` gets the
muted ·, and an uncaught ReferenceError shows ✕ with `settings:1` on the right.
* Workbench: expandable console lines, and Sources and Application panes
**Console lines summarise, then open.** The panel is a drawer on a phone, and the
messages worth the most space were the ones taking all of it — one uncaught stack
trace owned the whole pane and pushed the ten lines that led to it off the top. A
row is one line until it is asked for more. The chevron appears only on rows that
have more, decided by whether the collapsed text actually overflowed rather than
by guessing at a length.
**The origin is a link.** Reading "settings:1" and then going to find settings
line 1 by hand is the step a console exists to remove, so it opens Sources at that
line, highlighted. Console messages now carry the whole `sourceId` rather than the
file name they were being truncated to, because a bare "settings" matches nothing.
**Sources.** A WebView exposes no debugger protocol, so this asks the page: the
document and inline scripts it reads outright, external files it has the page
`fetch` — the same request the page already made, so usually a cache hit. Line
numbers, and a jump target that lands on the line. Known bound, and it is the
platform's: a cross-origin file served without CORS headers cannot be read back by
the page that loaded it, and the pane says so rather than showing an empty file
and letting you conclude the file was empty.
**Application.** Local storage, session storage and cookies, each key removable on
its own — the reason to open storage while debugging is usually to drop one key
and try again, not to wipe the site and lose the session that took ten minutes to
get into.
The five panes scroll horizontally; five chips squeezed to fit a phone are five
chips nobody can read.
Markers are aligned to the first line of the message they mark, by giving them the
message's line height instead of their own: a ✕ against a three-line error was
sitting halfway down it.
Verified on an Odin2 against duckduckgo.com/settings: a folded warning opens and
closes; an uncaught ReferenceError's `settings:1` switches to Sources, opens the
document and highlights line 1; the source list shows the document, external
scripts and inline scripts; an external script fetches and renders with line
numbers; Application lists the site's own localStorage key and removing it leaves
the section empty.
* Workbench: draw the DevTools pane switcher as a tab strip
It was a row of rounded pills with gaps between them, sitting a few pixels above
the terminal's tab strip and the editor's, both of which are flat, butt against
each other, and mark the active one by lifting it to `surfaceVariant` off a
`surface` rail. Two idioms for "pick one of these" in one window is one too many,
and the pills were the odd one out.
Same 36dp height, same 12dp padding, same label style as the strips it now sits
beside. The row still scrolls, because five panes do not fit across a phone and
squeezing them until none of them can be read is not the alternative anybody
wants.
* Workbench: put the console prompt at the end of the log, not on the panel floor
A browser's console prompt is the last row of the log, and the reason is that the
prompt and the answer to it are one conversation: what you typed, what came back,
and the caret waiting for the follow-up all read down a single column. Pinned to
the bottom of the panel it was a separate instrument, with the whole empty middle
of a mostly-quiet console between a result and the place you would type the next
thing.
It is a list item now, so it sits under the last message and moves down as output
arrives. The box around the field is gone with it — a filled, rounded input is a
form control, and this is a line in a log that happens to take typing. It gets the
same rail and the same "›" every other row has, because it is one of them.
Following the output now scrolls to the prompt rather than to the last message,
which is one row further up: the point of keeping up with a log is to end up
looking at the place the next thing gets typed.
Verified on an Odin2: with one warning on screen the caret sits directly beneath
it; typing `2*21` leaves `› 2*21`, `‹ 42`, and the prompt below both.
* Workbench: drop the console's level rails and its run button
The rail was a third statement of a fact already made twice. The marker says what
kind of line this is and the tint says it again for the two kinds worth noticing,
so a coloured stripe down the left of *every* row was decoration on a surface
whose whole job is to let two or three rows stand out from the rest — and it
started at the panel's edge, which is where the eye goes first.
The run button goes with it. The field submits on the keyboard's Go key, which is
how a console has always been submitted and is where the thumb already is after
typing; a play arrow floating at the end of an empty line was a second way to do
the same thing, taking up the one place a long expression could have run to.
Verified on an Odin2: `2*21` typed at the prompt and submitted with Go still
answers 42, and the levels still read at a glance from their markers.
* Workbench: console type follows the terminal's, and a narrower gutter
**Size.** The console had a number of its own — 11.5sp, picked to fit. It reads
`LocalTerminalFontSizeSetting` now, which is the same setting the terminal uses:
both are monospace logs of a machine talking back, they are two tabs of the same
drawer, and somebody who sized the terminal to something they can read on this
screen has already answered the question for the console. Changing one changes
both, which is the point — a pane that merely copied the terminal's *default*
would drift apart from it the moment anybody touched the setting.
Everything on the row is derived from that size rather than fixed alongside it:
line height is a proportion of the type, so a console set to 18sp does not end up
with its lines touching, and the count badge and the source link stay a step
smaller than the message instead of staying 10sp while the message grows.
**Gutter.** The marker column was 28dp off the front of every line — 8dp of
padding around a 20dp box holding one character — on a panel a phone can spare
about forty for. It is 18dp now, and the source link under a message follows it.
* Workbench: flatten the right panel's tab strip too
The last row of rounded pills in the workbench, and it sits directly above one of
the flat strips — the terminal's own tabs are a few pixels below it, so the two
ways of drawing "pick one of these" were visible in the same glance.
Flat now, on the same terms as the editor's tabs, the terminal's and the DevTools
pane switcher: tabs share edges instead of floating with gaps between them, the
active one lifts to `surfaceVariant`, and it fills the strip's height rather than
being a shape inside it. Icons and labels are unchanged, and Close stays pinned to
the right where scrolling cannot reach it.
`RightPanelChip` is `RightPanelTabItem`, because it is not a chip any more and a
name that says otherwise is the kind that survives three refactors.
* DevTools: a Network panel that sees the whole page, with payloads
The Network pane listed fetch and XHR and nothing else, so an ordinary
page produced an empty panel: the document, the scripts, the stylesheets
and the images are loaded by the browser, not by page script, and there
is no JS function in their path to wrap. Wikipedia showed zero rows.
Resource Timing sees exactly those, so the shim now runs a
PerformanceObserver alongside the fetch/XHR wrappers, with buffered:true
so entries recorded before it installed are replayed rather than lost.
The overlap between the two sources is settled on a timestamp taken when
the wrappers go in: a fetch that started after that is the wrappers' to
report, and would otherwise be drawn twice.
What each source can answer differs, and the panel says so instead of
pretending. Wrapped calls carry both sets of headers, the payload sent
and the body returned -- bodies capped at 16KB, skipped for binary and
oversized responses, and read from a clone so the page still gets its
own. Timing rows carry URL, kind, size and duration; the status code and
body are not exposed to a page for a resource it did not request, so the
detail view explains the gap rather than drawing empty sections.
Tapping a row opens it: General, request headers, payload, response
headers, response, with JSON pretty-printed. A filter strip counts the
rows by kind, which on a page with fifteen stylesheets is the difference
between a list and a wall.
The header's bare "Clear" link is now an overflow menu. Clearing was the
only thing in reach and the one thing that cannot be undone, while
"Preserve log" -- the setting you want set *before* the interesting
request happens -- had nowhere to live. Off by default, as in Chrome, so
each navigation starts clean. Copy-all joins them. ContextAction gained a
checked state to draw the tick; a boolean mode needs one, where a two-way
choice is still better said by naming the action.
Details worth their lines:
- Row names keep the query. A resource loader answers everything from one
path, so stripping it made fifteen rows all read "load.php".
- A zero transfer size is two different facts. With a body size it means
the cache answered; without one it means a cross-origin server declined
to say. The first says "cached", the second says nothing at all rather
than "0 B", which reads as a measurement.
- A navigation entry's duration runs to loadEventEnd, which is not set
until the load handlers return -- so the document row reported 0ms.
Verified on the Odin2 against Wikipedia: 26 rows across document, script,
css and img; live search XHRs captured at 200 with their JSON responses.
* DevTools: rebuild the Application pane around what a site actually keeps
The pane knew about three things -- local storage, session storage and
whatever document.cookie would admit to -- printed as one flat scroll
with values clipped at two lines and a Refresh link at the bottom, past
everything it refreshes. Most of what a modern site stores was simply
absent from it.
It now surveys eight, each collapsible and each stating its count in the
header, so the whole shape of what an origin keeps reads in one screen:
page origin and secure context, the two web storages, cookies, IndexedDB,
cache storage, service workers, and the web app manifest. A quota bar
sits at the top, because a site misbehaving over storage shows up as a
number there long before you would think to count cache entries.
Everything past the two storages is Promise-only, so the survey is
delivered through the JCodeDevTools bridge rather than returned from the
eval -- evaluateJavascript hands back what an expression evaluated to
synchronously, which for an async function is nothing. Each block is
guarded separately: caches and serviceWorker exist only in a secure
context, and a page on plain http would otherwise take the whole report
down with it.
Cookies are now read from both places they exist. document.cookie is what
the page can see; the WebView's own jar is what the server gets, and the
difference between the two lists is exactly the HttpOnly cookies -- which
on Wikipedia is four of seven, including the session cookie, none of
which this pane could previously show at all. They are badged, and
deleting one goes through the jar, the only side that can reach it.
Tapping a row opens the value in full, pretty-printed, with copy and
delete. That is the point of the rewrite: a two-line preview is enough to
recognise a key and never enough to debug one. Wikipedia's module store
is 948 KB of JSON that used to render as two clipped lines.
The overflow menu introduced for Console and Network now covers this pane
too -- Refresh, and Clear site data, which goes through
WebStorage.deleteAllData() because no page-side API can empty databases
the page never named.
Also: formatBytes stopped at MB and reported a 131 GB quota as
"134379.8 MB".
Verified on the Odin2 against Wikipedia (7 cookies, 4 of them HttpOnly)
and Excalidraw (3 IndexedDB databases with per-store record counts, a
59-entry workbox precache, an activated service worker, and the fetched
manifest).
* DevTools: colour page source, and make minified source readable
Sources and Elements both drew plain monospace text. A page script is a
source file, and JCode already knows how to colour one -- so they now go
through the same three layers the editor uses, in the same order of
authority: built-in Markdown and JSON, then an installed Dev Pack's rules
for the language, then the generic tokenizer so nothing is left unlit. A
Dev Pack that lights up a .js file in a tab now lights up the same file
when it arrives from a web page instead of the workspace.
On top of that, a language server's own classification when one is
already running: semantic tokens for a document that is not a file and
must not become one. It borrows a session the user's own work started,
matched by extension, announces the text under a URI in a directory that
does not exist, asks, and closes it again. Nothing is written and no
server is started. Failure is the ordinary case -- no server, none
running, none implementing semantic tokens, or one that declines a URI
with no file behind it -- and every one of them is silent, because the
tokenizer's colouring already stands on its own. A marker segment in the
URI keeps any diagnostics the server volunteers out of the Issues pane,
where they would name a file nobody can open.
Elements was worse than uncoloured: outerHTML serialises to one line, so
the whole document was a single row scrolling sideways forever. It is now
one tag per line, indented by depth. Script, style and preformatted
content passes through untouched -- re-wrapping a JavaScript body on its
angle brackets produces something that reads like code and is not the
code that ran.
And the thing that makes Sources usable on a real site: every script a
site serves is minified, so `{ }` in the header reformats one. Breaks only
on structural punctuation and only outside strings, template literals and
comments.
Three problems the device found, all in the minified path:
- `//` is not always a comment. In `url(https://…)` it is a scheme, and
reading it as a comment swallowed the rest of the file -- which for a
minified bundle is the whole file.
- Compose measures a no-wrap line by laying out every glyph in it, so a
400 KB row was an ANR before anything else ran. Lines are clipped at
1000 characters for drawing; `{ }` is how the rest becomes visible.
- Splitting a styled document per line with subSequence re-filters the
whole span list each call -- tens of thousands of spans against tens of
thousands of lines, and a hang. One pass over both, sorted, instead.
All colouring, reformatting and the server round-trip run off the main
thread, and the text is never waiting on any of it: plain first, then the
tokenizer, then the server if it answers.
Verified on the Odin2 against Excalidraw: inline scripts fully coloured,
the DOM snapshot one tag per line, and a 400 KB minified stylesheet
reformatted to ~15k coloured lines that scroll smoothly. The LSP layer is
exercised only on its silent-fail path -- this device has no language
server installed, so the positive path is unverified.
* Web Engine: JCode's own engine as an optional split, browser rewired onto it
The browser no longer renders on the device's system WebView — an engine the
app can neither choose, upgrade, nor trust (the Odin2 ships Chromium 101,
which drops dvh and renders Excalidraw as a black page). It now renders on
JCode's own engine: GeckoView 153, built as the :webengine dynamic-feature
split and delivered by the Web Engine marketplace extension
(jcode.ext.webengine), so the base APK stays at its current size and only
users who open web surfaces pay for the engine.
A split rather than a class-loader plugin because a browser engine needs
<service> entries for its child processes and manifests are fixed at install
time; as a split of our own package its manifest merges properly, native
libs land in the real lib dir, and classes join the app class loader on the
next start. WebEngineInstaller commits the split via PackageInstaller
MODE_INHERIT_EXISTING; the running process can't see new classes, so
NeedsRestart is its success state.
The seam (dev.jcode.webengine) keeps the base engine-agnostic: interfaces
only, reflective lookup, and WebEnginePlaceholder — the install prompt every
gated surface shows. Deliberately no system-WebView fallback.
Optimization is structural: WebEngineHost.get() starts nothing (Gecko boots
on the first createTab), one GeckoRuntime serves the process (a Gecko
invariant anyway), and prefers-color-scheme follows the workbench theme at
runtime instead of recreating the view. play-services-fido is excluded from
the GeckoView dep — its manifest demands Play resources the app doesn't
ship; costs passkeys, nothing else.
Known regressions this milestone, stated rather than hidden: the DevTools
panes were built on WebView plumbing (evaluateJavascript, injected shims,
the system cookie jar) that doesn't exist against Gecko, so the panel shows
an engine notice until the RDP client lands; the site-info panel loses its
per-site cookie/storage counts for the same reason (they read the *system*
WebView's jar, which is now simply the wrong store). Markdown preview and
extension UI hosts stay on WebView until their engine-backed
implementations exist — gating them now would break them with no
replacement.
Extension repo: github.com/blamspotdev/jcode-ext-webview (scaffolded).
* Web Engine: boot Gecko from the split it actually lives in
GeckoView assumes it was packaged in the base APK: GeckoThread's first
arguments are `-greomni getPackageResourcePath()`, and with the engine in
a feature split that path holds no omni.ja — Gecko segfaults at 0x30 in
GeckoThread.run the moment the runtime boots. Appending our own -greomni
via GeckoRuntimeSettings.arguments() loses; Gecko takes the first
occurrence. (Both measured on the Odin2; the arg assembly read from the
AAR's bytecode. In stock App Bundles only ABI/config splits exist and
assets stay in the base, which is why this topology is off Gecko's map.)
The path Gecko uses is asked of the Context it was created with, so the
runtime now boots against a ContextWrapper whose getPackageResourcePath()
answers with split_webengine.apk — and which returns itself from
getApplicationContext() so the wrapper survives Gecko's own hop. Every
Gecko payload lookup then lands where the payload actually is.
Verified on the Odin2: Excalidraw fully renders and takes ink on
GeckoView 153 inside JCode — the page the device's frozen system WebView
101 drew as black. Placeholder state (base installed without the split)
and lazy boot (zero GeckoRuntime activity until a web surface opens)
verified in the same session.
* Web Engine: drop the GeckoView split — pivot to Chromium
Reverts the two engine commits (e226bdc, 211264a) on the user's decision:
the bundled engine should be Chromium, not Gecko. No maintained embeddable
Chromium artifact exists (Chromium ships only as Chrome and as WebView
providers; Crosswalk and WebLayer both died here), so "Chromium" today
means the system WebView — which this revert restores in full: the
WebView-backed browser, its DevTools panes (console shims, network
capture, storage survey), site-info cookie counts, desktop-UA mode and
the theme-following web context.
The jcode-ext-webview extension stays as the home for a future
from-source Chromium split (//android_webview built into the same
:webengine seam this history carries); that build needs Linux
infrastructure and a standing security-release cadence, and starts only
if that cost is accepted.
On devices whose ROM pins an old provider (the Odin2's frozen 101), the
browser inherits that engine's limits again; the follow-up commits add
engine-version surfacing plus an install-latest-WebView flow to onboarding
and Settings, which is as far as any app can reach — the provider switch
itself is refused by that ROM's one-entry allow-list.
* Settings + onboarding: surface the web engine, offer the Play upgrade
The engine behind the browser and web previews is the device's WebView
provider — a system component the app can read but not choose. What an
app CAN do is make its state visible and the fix reachable, so:
- Settings → Web preview gains a "Web engine" card: current Chromium
version and provider package, an Outdated flag below Chromium 110
(108 shipped dvh — the line under which modern sites go from dated to
blank), a Play-Store install of the latest Android System WebView, and
a shortcut to Developer options for the provider switch. The card says
plainly that some devices lock the provider and JCode then stays on
the ROM's engine.
- Onboarding gains the same as a hint card, shown only when the engine
is below that line: setup is the one moment the user is already
granting things, and a provider switch made now saves a blank-page
mystery later. Nothing blocks; JCode works either way.
Verified on the Odin2: card reads "Chromium 101.0.4951.61 · Outdated ·
com.android.webview" with both actions; browser and the WebView DevTools
panes confirmed live again after the GeckoView revert.
* Browser: repair the zero-height viewport that blanked full-height sites
Some WebViews (measured on the AYN Odin2 — Chromium 101, and still 150) lay a
page out against a zero-height layout viewport: html,body{height:100%}, vh and
dvh all resolve to 0 while innerHeight, clientHeight and position:fixed report
the real size. A full-viewport app whose layout hangs off height:100%
(excalidraw and most canvas SPAs) then collapses to a blank pane, and nothing
looks wrong from the outside.
Inject a guarded shim on page start/finish that pins <html> to innerHeight px
so the 100% chain resolves, re-synced on resize. It arms only when the bug is
detected (a 100vh probe measures 0 while innerHeight > 0), so healthy WebViews
are untouched. Verified on-device: excalidraw renders, long pages still scroll.
* Add keyboard vector drawable for device settings
* Browser: nudge restored full-viewport pages to re-layout after the height pin
VIEWPORT_FIX_JS pins <html> to innerHeight so height:100% apps (excalidraw)
render on a WebView whose layout viewport resolves to 0. On a fresh navigation
the pin lands before the app lays out, so it renders. On a cold-start restore
the WebView is created at size 0 and the app lays out collapsed before the pin,
then never recovers on its own (a manual reload fixes it).
Fire a resize whenever the pinned height actually changes, so an app that
already laid out recomputes; guarded on a real change so the dispatched resize
can't re-enter a loop. Also extend the re-probe window to ~5s for SPAs that
mount after first paint.
* Browser: don't force the DevTools drawer open on a plain "Open Browser"
Opening the built-in browser from the Command Palette popped the DevTools
right-drawer open over it, because both the URL-carrying opens (previews,
terminal/markdown links) and the plain no-URL open shared one revealSignal, and
an effect revealed DevTools on any bump.
Split it: a new devToolsRevealSignal is bumped only by requestOpen(url) (the
opens where DevTools is part of the intent), and the reveal effect watches that.
The palette's requestOpen() bumps only revealSignal, so it surfaces the browser
tab without forcing DevTools open. The DevTools tab stays available to open by
hand once the browser has been opened.
* Browser: fix Excalidraw menus/modals collapsing under the vh-unit WebView bug
The Odin2 WebView resolves every CSS viewport-length unit (vh/dvh/svh/lvh and vw kin) to ~0, while innerHeight, documentElement.clientHeight, percentage heights and position:fixed offsets all report the real size. The earlier html-pin fixed height:100% chains (the canvas) but not overlays sized in vh: Excalidraw runs in mobile mode here, where its menu and modals are viewport-height drawers, so they shrank to a sliver.
Extend VIEWPORT_FIX_JS: expose the real 1vh/1vw as --jcode-vh/--jcode-vw pixel custom properties (kept in step on resize) and rewrite every vh/vw-family unit in the page's own CSS -- stylesheets, grouped @media/@supports rules, inline styles and custom properties -- to calc(var(--jcode-vh) * N), verified on-device to resolve where the bare unit gives 0. A MutationObserver catches stylesheets/nodes added on demand. Still arms only when a 100vh probe reads under half of innerHeight, so healthy WebViews are untouched.
* Browser: stop force-darkening self-theming sites (Excalidraw light UI came out dark)
isAlgorithmicDarkeningAllowed was tied to the workbench theme, so a dark JCode force-darkened every page's DOM. Force-dark cannot touch a <canvas>, so a self-theming app viewed in its own LIGHT theme rendered split: Excalidraw's white canvas under a darkened toolbar and menu. Disable it and let the page paint its own colours, which is what a mobile browser does and is correct for both light and dark sites. prefers-color-scheme still follows the workbench theme (via the themed context), so a site set to 'system' still matches JCode.
* Browser: pin height/orientation @media to the real viewport (Excalidraw welcome screen)
The same WebView bug that zeroes the vh unit also makes @media evaluate the viewport HEIGHT as 0 (width is fine): (max-height:Npx) always matches, (min-height:Npx) never does, and orientation reads landscape. Excalidraw gates its empty-canvas welcome screen behind a max-height query, so it was hidden as if the screen were 0px tall — blank where desktop shows the logo and hints.
When the shim arms, re-evaluate each height/orientation media feature against the real innerHeight/innerWidth and rewrite the rule to a constant that matches (min-width:0px for true, min-width:99999px for false) — what the page would do on a real phone of this size. Done once per rule and deliberately NOT on resize: re-mutating mediaText on the resize a rotation fires wedged Excalidraw into a blank canvas until reload, and the visual-viewport pin already carries layout across a rotation. Trade-off: a rotated window keeps the media verdict from the orientation it loaded in. Verified on-device: welcome screen renders, and a full portrait<->landscape rotation no longer blanks the page.
* Browser: recover from the WebView's intermittent black reload surface
This device's WebView returns from a reload with a black, uncomposited content surface on a minority of loads. Measured: even a position:fixed probe fails to paint on those loads, so it is the GPU surface, not the page's layout — and it is pre-existing, reproducing on a minimal pin-only shim that predates the vh/@media work (so the CSSOM rewrite did not cause it). The pin already cuts it from ~100% (Excalidraw needs a definite html height or its height:100% chain collapses) to ~30%.
Re-applying the pin and dispatching a resize on each post-arm interval tick nudges the page to re-render once it has mounted, forcing a fresh frame that unsticks the surface; removing those more than doubled the black rate in testing. Run them over a longer (~9s) window to catch a late-mounting SPA. The resize goes only through the media-free resize handler, so it cannot re-trigger the rotation blank. On-device this took the black-reload rate from ~30% to ~19%; it is a mitigation, not a cure — the residual is a hardware/WebView compositor fault a reload still clears.
* Browser: make "Request desktop site" lay out at a desktop width, not just send the UA
The toggle only swapped the user agent, so the layout viewport stayed the phone's — measured: with the desktop UA in place, innerWidth was still 468. A site that reads the agent got its desktop stylesheet applied to a 468px column and spilled off the right edge (Google's results lost two thirds of their width), and a responsive site honoured its own width=device-width and handed back the very mobile layout the toggle was asked to escape (GitHub, Stack Overflow, MDN were byte-identical either way). Neither is what "desktop site" means.
Add the other half: rewrite (or add) the page's viewport meta to a fixed 1280px, re-asserted through a MutationObserver because frameworks rewrite the tag, and then zoom the WebView so that width fits the screen. The zoom is applied after load via zoomBy rather than declared, because setInitialScale and an injected initial-scale are both read off the first viewport tag the parser sees — the site's own — and were ignored on every page that ships one. Verified on-device: Google, GitHub, Stack Overflow and MDN all render their real desktop layouts, full width, nothing cut off. 1280 rather than 1024 because GitHub's marketing nav switches above 1024.
* VSIX host: render OpenChamber in its touch layout, matched to the dark IDE
OpenChamber's VS Code bundle hard-codes runtime.isVSCode = true and pins a
desktop layout from that alone, ignoring the coarse pointer and ~469px width, so
its dialogs came up as clipped desktop modals with keyboard hints instead of
touch bottom sheets. It exposes no ?surface=touch opt-out, so intercept the write
to its __OPENCHAMBER_RUNTIME_APIS__ global and clear the flag. The host transport
is unaffected — it keys on acquireVsCodeApi, not this flag.
Clearing the flag also drops OpenChamber's VS Code theme, leaving "system" mode
to resolve via prefers-color-scheme (reported light here). Report dark for just
that query so "system" tracks the dark IDE, passing every other media query
through so the pointer/hover checks still yield the touch layout.
* Revert "VSIX host: render OpenChamber in its touch layout, matched to the dark IDE"
This reverts commit d575767. Per user feedback, JCode should not override a
third-party VSIX extension's own layout decision. OpenChamber renders in its
native (VS Code / desktop) layout again and reads the dark theme we already
supply through the --vscode-* variables, so the paired prefers-color-scheme
shim is unnecessary too.
* Extensions: install & update VSIX extensions from custom GitHub sources
Add "Extension Sources" — user-added GitHub repos whose releases publish a .vsix.
JCode resolves each repo's newest .vsix release, installs or updates the extension
from it, and shows "Update available" in the main Extensions list, the same flow
as the built-in marketplace. This is how a VSIX extension like OpenChamber gets
updated inside JCode, where the host owns extension updates and the extension's
own in-app updater does not run (it's gated to desktop/web runtimes).
- ProviderReleaseFetcher: GitHub releases API -> newest .vsix asset + version,
mirroring UpdateChecker's transport and SemVer compare. Repo parse is
case-insensitive (soft keyboards autocapitalize "github").
- ExtensionInstaller.installVsixFromUrl reuses the already origin-agnostic
openStream(url) + installFromVsixBytes; MarketplaceEntry gains an optional
vsixAssetUrl so a source-sourced install/update routes through it.
- MainViewModel: persisted source list + installed-id -> source attribution
(cleared on uninstall); source releases fold into marketplaceEntries so the
existing marketStatus/isUpdateAvailable badge logic lights up with no UI change.
- UI: a Sources button in the shared ManagerPanelHeader opens an Extension
Sources editor page (add/remove repos, per-source latest release + Install/
Update), following the ExtensionPermissions page pattern.
* Extension Sources: restyle with JCode's shared manager design kit
The page was raw Material components (bulky OutlinedTextField, plain text lines,
FilledTonalButtons) that read as a bolt-on. Rebuild it from the same kit the
SDK/LSP/Extension managers use so it belongs to the Extensions surface:
- Header matches ManagerPanelHeader (title + source count + refresh icon).
- Each source is a surfaceVariant card with the installed extension's icon
(ExtensionIcon), repo + name, and the shared ManagerStatusChip (the same
Installed / Update available pill as the main list), then ManagerSummaryRow
lines (Latest release / Installed / Asset) and CompactFilled/OutlinedButton
actions.
- Add-source row uses a compact bordered field styled like CompactSearchField
(minus its search glyph) plus a CompactFilledButton, with a lock-icon trust
note. No behavior change.
* Extension Settings: header + per-extension icons, matching the manager kit
Bring the Extension Settings page in line with the extensions list and the new
Extension Sources page: a title + "N installed" header, and each collapsible
card now leads with the extension's icon.
ManagerSectionCard gains an optional `leading` slot (default null, so every other
caller is unchanged) rendered before the title; ExtensionPermissionsPage passes
the ExtensionIcon. No behavior change to the settings/permissions controls.
* Extensions: adopt the App Settings look — primary-blue section headers
Reorganize the Extension Sources and Extension Settings pages under the same
primary-tinted section headers App Settings uses, so they read as first-class
settings surfaces:
- New shared ManagerGroupHeader (labelLarge, primary, semi-bold, optional
trailing action), matching SettingsGroup's header.
- Extension Settings: drop the page title; group installed extensions by type
under "Apps" / "Language packs" / "Source control" / … headers, cards keep
their icons and collapse.
- Extension Sources: "Add a source" and "Sources" (refresh moves onto its
header) sections instead of a floating title + card.
No behavior change.
* VSIX: surface contributes.configuration as extension settings
A VS Code extension declares its user settings under contributes.configuration,
but JCode dropped them on import — so an extension like OpenChamber showed only
Activation/capabilities on the Extension Settings page, none of its own settings
(apiUrl, opencodeBinary).
- VsixPackage.parse translates contributes.configuration (object or array form)
into ExtensionSetting[] — full dotted key kept verbatim, JSON-schema type mapped
(boolean/number/enum/string), default + enum + NLS-resolved description carried
over; toExtensionYaml emits them as the `settings:` block. That flows into
InstalledExtension.settings, so the settings render on the page and the existing
config.* extension API resolves them.
- The VSIX host was activated with an empty configuration, so getConfiguration
always returned fallbacks. It now seeds from config.all (defaults overlaid with
the user's saved values), delivering the values to the extension.
Verified on device: reinstalling OpenChamber from its source regenerates
extension.yaml with the settings, and Api URL / Opencode Binary appear on its card.
* Extension Sources: drop the redundant intro line
The "Add a GitHub repo…" blurb duplicated what the section headers and the trust
note already convey. The page now opens straight to "Add a source".
* Extension Settings: richer cards with an activation-status pill
The collapsed cards were sparse (icon + name + author) and, at low surface alpha,
barely separated from the OLED background. Rebuild each as a defined card:
- Rounded 12dp corners + a hairline border + slightly higher surface, so cards
read as distinct objects on black.
- Metadata subtitle (description · version · VSIX) instead of just the author.
- An activation-status pill on the right — Auto-start (green) / On-demand (blue)
/ Manual (grey) with a status dot — reading LocalExtensionActivation, so the
collapsed card shows on/off at a glance and fills the dead space.
- A compact "N installed" count chip in the header (replacing the intro line).
Expands to the same settings/activation/capabilities as before.
* Extension Sources: match the defined-card styling
Give the source cards the same treatment as the Extension Settings cards —
rounded 12dp corners, a hairline border, slightly higher surface, and a 38dp
icon — so the two Extensions pages read as one surface.
* App Settings: match the defined-card styling
Give SettingsCard (and WarningCard) the same rounded 12dp corners, hairline
border, and slightly higher surface as the Extension Settings/Sources cards, so
every settings-style surface reads the same.
* Themes: replace built-ins with Catppuccin, Pierre Dark, Night Owl
Swap Dracula → Pierre Dark and Midnight OLED → Night Owl, and make Pierre Dark
the default. Palettes taken from the upstream themes:
- Pierre Dark (github.com/pierrecomputer/theme): #0a0a0a canvas, #171717 panels,
#fafafa text, #009fff accent with magenta/green highlights.
- Night Owl (Sarah Drasner): #011627 navy canvas, #d6deeb text, #82aaff / #c792ea
/ #7fdbca accents.
A previously-saved "midnight"/"dracula" selection falls back to the new default
(Pierre Dark) via ThemeBundleRegistry.byId, so no migration is needed.
* Tooltip: open under the control, after a short delay
JcTooltip anchored above and opened instantly on hover/press. Now, globally:
- a below-anchor PopupPositionProvider centers the label under the control (it
flips above only when there's no room below), and
- a 500ms hover/long-press delay keeps a passing cursor quiet.
Gestures are driven here with an observe-only pointer pass (enableUserInput off),
so the wrapped button still gets its own clicks; the one swallowed case is a
touch long-press that already opened the label, so reading a tooltip never also
taps the button.
* Ext Dev: flat underline tab strips, browser-devtools style
The Inspector/Validator/Log and extension-selector rows were filled pills. Make
them flat tabs — muted labels with a primary underline hugging the active one,
sitting flush above the strip's divider — matching a browser devtools tab strip.
* Run panel: compact pill tabs for the Run/Build toggle
Replace the full-width segmented bar with the shared ManagerFilterChip, so
Run/Build reads as two compact pills instead of a heavy segmented control.
* Ext Dev: use the DevTools filled-tab strip, not an underline
Match the Console/Sources/Network strip in the DevTools panel: the active tab
lifts to surfaceVariant off the surface rail, full-height and butted against its
neighbours, instead of the flat underline. One tab idiom across the panels.
* DevTools Network: show the HTTP status on browser-loaded resources
Resource-timing rows (CSS, images, beacons the browser fetched itself) reported
no status, so they read "—". Capture PerformanceResourceTiming.responseStatus
(Chromium 109+) for resource and navigation entries; fetch/XHR already carry it.
Cross-origin responses without Timing-Allow-Origin still read 0 → "—", which the
browser genuinely won't disclose. Detail-pane copy updated to match.
* Workspace header: folder icon for the Open folder button
It was sharing the DatasetLinked (linked-boxes) glyph with Sources/
Destinations/Database, which reads as nothing like a folder next to
its 'Open folder' tooltip. Give it a dedicated OpenFolder slot mapped
to FolderOpen.1 parent a16fc7c commit 42019ba
76 files changed
Lines changed: 8497 additions & 670 deletions
File tree
- app/src/main
- aidl/dev/jcode/vdevice
- assets/vdevice
- java/dev/jcode
- adb
- lsp
- vdevice
- workbench
- marketplace
- res
- drawable
- values
- core/design/src/main/java/dev/jcode/design
- feature
- editor-pane/src/main/java/dev/jcode/feature/editor/pane
- marketplace/src/main/java/dev/jcode/feature/marketplace
- onboarding/src/main/java/dev/jcode/feature/onboarding
- settings/src/main/java/dev/jcode/feature/settings
- tools
- hardware-fixture
- src/dev/jcode/vdevice/hwfixture
- vdevice-keyboard
- res
- drawable
- font
- values
- src/dev/jcode/vdevice/keyboard
- vdevice-settings/res/drawable
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
47 | | - | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
48 | 53 | | |
49 | 54 | | |
50 | 55 | | |
| |||
Lines changed: 8 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
4 | 11 | | |
5 | 12 | | |
6 | 13 | | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | 14 | | |
Binary file not shown.
Binary file not shown.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
| 85 | + | |
85 | 86 | | |
86 | 87 | | |
87 | 88 | | |
| |||
284 | 285 | | |
285 | 286 | | |
286 | 287 | | |
287 | | - | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
288 | 291 | | |
289 | | - | |
290 | 292 | | |
291 | 293 | | |
292 | 294 | | |
| |||
455 | 457 | | |
456 | 458 | | |
457 | 459 | | |
| 460 | + | |
| 461 | + | |
458 | 462 | | |
459 | 463 | | |
460 | 464 | | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
461 | 471 | | |
462 | 472 | | |
463 | 473 | | |
| |||
710 | 720 | | |
711 | 721 | | |
712 | 722 | | |
| 723 | + | |
713 | 724 | | |
714 | 725 | | |
715 | 726 | | |
| |||
1918 | 1929 | | |
1919 | 1930 | | |
1920 | 1931 | | |
| 1932 | + | |
| 1933 | + | |
| 1934 | + | |
| 1935 | + | |
| 1936 | + | |
| 1937 | + | |
| 1938 | + | |
| 1939 | + | |
| 1940 | + | |
| 1941 | + | |
| 1942 | + | |
| 1943 | + | |
| 1944 | + | |
| 1945 | + | |
| 1946 | + | |
| 1947 | + | |
| 1948 | + | |
| 1949 | + | |
| 1950 | + | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
| 1955 | + | |
| 1956 | + | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
| 1963 | + | |
| 1964 | + | |
| 1965 | + | |
| 1966 | + | |
| 1967 | + | |
| 1968 | + | |
| 1969 | + | |
| 1970 | + | |
| 1971 | + | |
| 1972 | + | |
| 1973 | + | |
| 1974 | + | |
| 1975 | + | |
| 1976 | + | |
| 1977 | + | |
| 1978 | + | |
| 1979 | + | |
| 1980 | + | |
| 1981 | + | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
| 1990 | + | |
| 1991 | + | |
| 1992 | + | |
| 1993 | + | |
| 1994 | + | |
| 1995 | + | |
| 1996 | + | |
| 1997 | + | |
| 1998 | + | |
| 1999 | + | |
| 2000 | + | |
| 2001 | + | |
| 2002 | + | |
| 2003 | + | |
| 2004 | + | |
| 2005 | + | |
| 2006 | + | |
| 2007 | + | |
| 2008 | + | |
| 2009 | + | |
| 2010 | + | |
| 2011 | + | |
| 2012 | + | |
| 2013 | + | |
| 2014 | + | |
| 2015 | + | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
| 2019 | + | |
| 2020 | + | |
| 2021 | + | |
| 2022 | + | |
| 2023 | + | |
| 2024 | + | |
| 2025 | + | |
| 2026 | + | |
| 2027 | + | |
| 2028 | + | |
| 2029 | + | |
| 2030 | + | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
| 2034 | + | |
| 2035 | + | |
| 2036 | + | |
| 2037 | + | |
| 2038 | + | |
| 2039 | + | |
| 2040 | + | |
| 2041 | + | |
| 2042 | + | |
| 2043 | + | |
| 2044 | + | |
| 2045 | + | |
| 2046 | + | |
| 2047 | + | |
| 2048 | + | |
| 2049 | + | |
| 2050 | + | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
| 2069 | + | |
| 2070 | + | |
| 2071 | + | |
| 2072 | + | |
| 2073 | + | |
| 2074 | + | |
| 2075 | + | |
| 2076 | + | |
| 2077 | + | |
| 2078 | + | |
| 2079 | + | |
| 2080 | + | |
| 2081 | + | |
| 2082 | + | |
| 2083 | + | |
| 2084 | + | |
| 2085 | + | |
| 2086 | + | |
| 2087 | + | |
| 2088 | + | |
| 2089 | + | |
| 2090 | + | |
| 2091 | + | |
| 2092 | + | |
| 2093 | + | |
| 2094 | + | |
| 2095 | + | |
| 2096 | + | |
| 2097 | + | |
| 2098 | + | |
| 2099 | + | |
| 2100 | + | |
| 2101 | + | |
| 2102 | + | |
| 2103 | + | |
| 2104 | + | |
| 2105 | + | |
| 2106 | + | |
| 2107 | + | |
| 2108 | + | |
| 2109 | + | |
| 2110 | + | |
| 2111 | + | |
| 2112 | + | |
| 2113 | + | |
| 2114 | + | |
1921 | 2115 | | |
1922 | 2116 | | |
1923 | 2117 | | |
| |||
2060 | 2254 | | |
2061 | 2255 | | |
2062 | 2256 | | |
| 2257 | + | |
| 2258 | + | |
| 2259 | + | |
| 2260 | + | |
| 2261 | + | |
| 2262 | + | |
| 2263 | + | |
| 2264 | + | |
| 2265 | + | |
| 2266 | + | |
2063 | 2267 | | |
2064 | 2268 | | |
2065 | 2269 | | |
| |||
5455 | 5659 | | |
5456 | 5660 | | |
5457 | 5661 | | |
| 5662 | + | |
5458 | 5663 | | |
5459 | 5664 | | |
5460 | 5665 | | |
| |||
0 commit comments