Skip to content

Web ui3 - #1179

Open
JeanMertz wants to merge 12 commits into
web-ui2from
web-ui3
Open

Web ui3#1179
JeanMertz wants to merge 12 commits into
web-ui2from
web-ui3

Conversation

@JeanMertz

Copy link
Copy Markdown
Collaborator

🚧

A conversation created by a delegated plugin query kept whatever title
the plugin passed, and nothing else. Started from the web UI, where no
title is passed at all, every conversation stayed "Untitled" forever,
while the same prompt through `jp query` would have been named from its
leading heading or by the title model.

The delegated path takes the same two routes as `jp query`. A leading
markdown heading is written to the conversation before the turn starts;
anything else hands `conversation.title.generate` a task that runs
alongside the turn, so the name lands in the list while the answer is
still streaming. A title model that fails is logged and skipped rather
than taking the turn down with it.

`TitleGeneratorTask::generate` is the entry point for a caller that
already holds the conversation lock: it asks the model and hands the
title back instead of writing it, which the delegated path needs
because the lock belongs to the turn.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The three page scripts lived in raw Rust string literals, where nothing
could look at them. A stray double quote ended the literal, a
redeclaration between two scripts on the same page went unnoticed, and
the first sign of either was a blank page and a browser console. The
scripts also had to avoid double quotes entirely, which is why every
inline SVG attribute is single-quoted.

Each script is a file next to the view that serves it, pulled in with
`include_str!`, so the bytes reaching the browser are unchanged. A
build script parses them with `oxc` and applies the spec's early-error
rules, grouped per page because classic scripts share one global
lexical scope: a name declared in `list.js` collides with the same name
in `filter.js`, and neither file is wrong on its own. Diagnostics are
reported as `cargo::error` directives, so a broken script fails
`cargo build` with a line and column.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Reaching the controls around a transcript meant reaching for the mouse.
Single keys now stand for them: `r` quotes the selection, `c` opens the
configuration picker, `e` opens the larger field, `/` focuses the
composer, `[` and `]` step between turns, and Home and End jump to
either end. Escape leaves the composer, so the keys are reachable again
after typing. Each one clicks the control it stands for rather than
calling its handler, so a shortcut cannot drift from the button. They
are held back while a field has focus or a dialog is open.

Quoting also survives a touch: the tap that reaches the Quote button is
the tap that dismisses the selection, so the handler ran with nothing
selected and did nothing at all. The last selection made inside the
transcript is remembered and used as the fallback, then dropped, so a
second tap with nothing selected does not quote the same passage twice.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The build script parses the page scripts and applies the spec's early
errors, which catches a syntax error or a redeclaration but nothing
about the DOM: a misspelled property, a call with the wrong arity, or a
method that does not exist on the element it is reached through all
build and then fail in the browser.

`just typecheck-js-ci` runs `tsc` over `crates/plugins/command/
serve-web/tsconfig.json` with `checkJs`, which reads the scripts as
JavaScript against the DOM type definitions and emits nothing. The
workflow runs it as its own matrix task, gated on `.js` and
`tsconfig.json` changes, and installs Node only for that task.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
A table wider than the column overflowed into `.conversation-detail`,
whose `overflow-y: auto` makes the horizontal axis scrollable too, so
dragging the table dragged the entire transcript sideways.

Every rendered table is wrapped in a `.table-scroll` div that scrolls
on its own, with `overscroll-behavior-x: contain` so a swipe past
either end stops there rather than reaching the transcript or the
browser's back gesture. The wrapper carries a shadow on whichever side
still has table beyond it: iOS shows no resting scrollbar, and a cut
that lands on a column border reads as the end of the table. The
shadows are masked by covers painted in the surface colour, which is
why each block that holds rendered markdown declares `--surface`.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The conversation rows painted over the header as they scrolled under
it, so the header read as if it had no background at all. A sticky
element with `z-index: auto` creates no stacking context, which leaves
the later elements in the list free to paint on top of it. Giving the
header a `z-index` puts it where a sticky header belongs.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Developing the web UI from a checkout took a manual build, a manual
copy into JP's plugin directory, and a manual restart between every
edit. `just serve-web` does the first two and starts the server.

`just serve-web-watch` keeps it running across edits. A plain
`cargo watch -s 'just serve-web'` cannot be used here: a turn started
from the browser runs inside the host process the plugin is attached
to, so restarting on a file change aborts whatever the assistant was in
the middle of, including the assistant editing these very files. The
watcher instead rebuilds, waits for the server to fall idle — no
conversation lock naming its pid, and `/status` reporting nothing in
flight — and only then stops it, leaving a supervisor loop to start the
new build. A tree that does not compile is left alone, and the
supervisor waits on a pipe until a change fixes it.

`_install-serve-web` writes the binary into JP's plugin directory
rather than `cargo install`-ing it: `jp` resolves a plugin from its own
plugin directory first, and `serve-web` is a registry plugin, so
anything the registry put there would shadow a build in `~/.cargo/bin`
forever.

    just serve-web --port 4000
    JP_SERVE_WEB_WORKSPACE=~/code/other just serve-web-watch

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The ticket command has no format flag of its own; machine-readable
output comes from `jp -F json`, which switches the whole invocation.
The recipe documentation still advertised `--json`, and
`ticket-promote` passed it, so promoting a ticket failed on an
unrecognized argument.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
`ticket-promote` seeds an RFD draft and then marks the ticket as
promoted. A run interrupted between the two leaves the ticket unmarked,
so running it again seeds a second draft for the same ticket and leaves
the first one orphaned.

The seeded Summary names the ticket it came from, which makes it
findable: a retry looks for that line under `docs/rfd`, links the draft
it finds, and stops instead of seeding another.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Signed-off-by: Jean Mertz <git@jeanmertz.com>
The configuration chooser is an ordered list: each row is either one
of the configurations on the load paths or a `--cfg` argument written
out, and the order of the rows is the order the host applies them, so
a later row overrides an earlier one. A row is dragged by its grip, or
moved with the arrow keys while the grip has focus. Until now names
were applied before assignments whatever the reader meant, with no way
to say otherwise.

`+ Configuration` opens a picker over the load paths: type to filter,
Enter ticks the first match, and `⌘ Enter` adds everything ticked, in
the order it was ticked. `+ Value` adds a row to write an argument
into, which is one field rather than a key and a value, so `@path`, a
JSON object and the reset keywords all reach `--cfg` now. On the
new-conversation page `⌘ Enter` starts the conversation.

The chooser needs JavaScript: a list that starts empty and is filled
by buttons has nothing to degrade to. The rest of both pages still
works without it. The build script also checks the chooser's script,
which it had never been told about.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Sending a message from the web UI while a turn is running no longer
fails with "A turn is still running. Stop it first, then send." The
assistant is interrupted where it is, whatever it had produced is kept
as context, and the message becomes the next thing it reads, inside the
turn that was already going. This is what Ctrl-C then `[r] Reply` does
at a terminal.

Staying inside the turn is what makes it reliable. The conversation is
never unlocked in between, so there is no window in which the message
could be refused as already-locked, and no need to guess when a stopped
turn has finished unwinding. A message that arrives just as a turn ends
is told so, and starts a turn of its own rather than being lost.

The web UI's stop button now also takes effect while the assistant is
streaming, instead of when the stream ends on its own. An interrupt
naming a conversation used to reach only the handler that owns the gaps
between turn phases, which read it at the next phase boundary.

`jp` and its plugins must be updated together: `interrupt` carries an
`action` (`stop`, `abort` or `reply`) and the protocol is now 9. An
`interrupt` without one still means stop, so an older plugin keeps
working; a plugin needing 9 is refused by an older `jp` at the
handshake, with a message saying which is stale.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant