| 2026-11-07 |
webclient: read the skills catalog as documents, split it into two tabs
...
Settings had one long panel mixing skills and protocols as raw markdown,
which was hard to read and wasted the width. Skills and Protocols are now
two tabs over one panel keyed by kind.
Selecting an entry shows the parsed document — triggers, preconditions,
steps, post-checks, escape hatches, body — and an Edit button switches to
the source, so the catalog is read the way the agent loads it. The
single-entry payload carries the parsed document; the list still does not,
so it stays cheap. SkillDocument is shared with the chat proposal card,
which now only draws its own frame and header.
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: make the protocol proposal card a document, not a kit tile
...
GnCard is a 340px tile with an uppercased title and GnSteps uppercases
every step, so a protocol document came out as a narrow column of caps with
its body shown as raw markdown. The card is a chat domain card now: an
app-owned full-width frame (accented left edge for pending/accepted/
rejected) with the kit's buttons, alert, chips, badge and loader.
- shows what was missing: when to use it (triggers), preconditions, steps
with their action and check, post-checks, and the body as rendered
markdown under "Context";
- unwraps a step the model wrote as a YAML mapping, which the parser turns
into Python repr text, so the step reads instead of leaking {'id': ...};
- says "Add to catalog" and shows an "In the catalog" badge once decided.
Eugene Sukhodolskiy
committed
1 day ago
|
webclient: reach the socket from cards, and draw the proposal card with the kit
...
The skill-proposal card sent its decision through inject('wsSend'), but no
ancestor ever provided it — the click went nowhere and the card reported
"No connection". Form.vue carried the same latent bug since 9dfdb48.
- wsBridge.js: a module-level bridge. useWebSocket registers its send on
setup and clears it on unmount; cards call wsSend() and get false back
instead of silently dropping the payload.
- Form.vue uses the bridge and reports a refused send without marking the
form submitted.
- SkillProposal.vue is rebuilt on the kit (GnCard, GnAlert, GnChip, GnList,
GnSteps, GnDescriptionList, GnLoader, GnButton) and exports `bare`, so
UiComponentCard no longer wraps it in a second frame with a dev header.
- registry.js tracks bare components; UiComponentCard renders them as-is.
Eugene Sukhodolskiy
committed
1 day ago
|

skills: agent-written skills and protocol proposals with chat buttons
...
A skill the agent writes now lands in the catalog on its own; a protocol
never does. `skills(op="propose")` stores the document in `skill_proposals`
and returns a card through the tool result's `ui_component` metadata — no
`render_component` call, so profiles without `navi_ui` get the card too.
The decision is the server's: the browser sends `skill_proposal` over the
WebSocket, `SkillStore.decide_proposal` writes the catalog row from the
*stored* document, names the clicking user as the author, and answers with
`skill_proposal_decided`. The agent learns the outcome as a hidden user
message, the same path a `navi_ui` form submission takes.
`create`/`update` refuse protocol documents and point at `propose` with
`replaces_id`, so the version history of a protocol always starts from a
user's yes. The chat store now renders `ui_component` from any tool whose
result carries one, not only from `mcp__navi_ui__render_component`.
Eugene Sukhodolskiy
committed
1 day ago
|
| 2026-11-02 |
webclient: one audio player, so a published card sounds like the viewer
...
The published-content card fell back to the browser's own grey bar while the
full-screen viewer had a player with the track's name, times, seek and
volume. The card now shows that player too, tightened a little for a body
that sits under a summary row.
The player moved out of ContentLightbox unchanged: two copies would have
drifted, and it is the same file in both places.
Eugene Sukhodolskiy
committed
6 days ago
|
| 2026-11-01 |

webclient: a viewer for every content type, and a message sent mid-turn waits its course
...
The lightbox is now one dialog for every file the artifacts panel and the
content cards can show: audio and video players on the kit's own controls, a
text/source view with highlight.js, and a three.js viewer that replaced the
standalone stl.html frame with a model one. highlight.js emits .hljs-* classes
and ships no theme, so until now every highlighted block rendered in one flat
colour — app.scss now carries the palette.
While a turn runs, the send button no longer turns into Stop and vanishes the
draft with it: the message goes into the socket and is marked "queued" until
the stream_start of the turn that consumes it, the write-now-run-next model
navi-code uses. Send and Stop sit side by side, and the queue chip takes the
larger of the local and the server's counts.
The socket keeps its own liveness: a ping every 20 s, a watchdog that reopens a
socket silent for 45 s or stuck in CONNECTING, and a reconnect on resume from
background, network return or bfcache. On reopen it drops a half-written
message left by the dead socket, so the session_sync that follows is not hidden
behind a stream that will never finish.
Eugene Sukhodolskiy
committed
7 days ago
|
| 2026-10-10 |

webclient: the push permission is asked once a session, after signing in
...
The permission was only ever asked from the switch in Settings, and a call
without a user gesture is ignored by Firefox and Safari and answered with a
quiet icon by Chrome — so the app now offers a bar whose button click is that
gesture. It is asked once per browser session (there is no marker that tells a
fresh sign-in from a reload, so the session is the honest bound), and a
refusal ends it for good: only an explicit block counts, and after that the
switch in Settings is the single way back to it.
The policy lives in useNotificationPrompt.js as a layer over usePush, which
is untouched: the granted-but-unsubscribed trap is what makes the eligibility
gate "permission is still default", otherwise the bar would silently
re-subscribe whoever turned notifications off by hand. The banner itself is
presentation only, and is the component proposed to gnexus-ui-kit
(docs/tasks/gn-permission-banner.md there) — the kit takes the gesture, the
view and the contract, never the storage keys or the timing.
Eugene Sukhodolskiy
committed
29 days ago
|
| 2026-10-05 |
webclient: W4 — chat empty state -> GnEmptyState, card modal -> GnModal
...
- ChatArea: .empty-chat custom icon+text -> GnEmptyState card
(keep .empty-chat wrapper only for centering)
- CardGrid: inline Teleport modal (overlay/close/scroll bespoke chrome
removed) -> GnModal; .card-modal/.card-modal-title class names kept
inside the new DOM (test contract); kit header slot renders the title
- WelcomeScreen: typography aligned to kit (30px h1, 14px base),
profile cards get -radius-md, dead .profile-icon style dropped
- CardGrid test: mount with the kit plugin (GnModal resolves),
two nextTicks for the post-flush open watcher
Verified: vitest 111 passed, build OK, welcome screenshot unchanged
(modal/empty-state visually checked after final build)
Eugene Sukhodolskiy
committed
on 5 Oct
|
| 2026-07-09 |

feat: integrate navi_ui MCP server (card_grid + form) into master
...
Port the internal navi_ui MCP server from the vmkdemo branch (it never
landed on master). The server exposes render_component, which returns a
structured JSON envelope; navi/mcp/tools.py extracts metadata.ui_component
onto the role="tool" message, and the webclient renders the component
(card_grid, form) inline inside the assistant turn.
Backend
- navi/mcp/ui_server/: FastMCP server + component registry with card_grid
and form components (pydantic-validated payloads, LLM-friendly schema docs)
- mcp_servers.d/navi_ui.json: streamable_http config, group "ui"
- config.py: navi_ui_mcp_enabled/host/port flags
- main.py: start UI server in lifespan (task + wait-for-ready + cancel)
- mcp/tools.py: navi_ui envelope parsing; "Error:" results surface as
failed tool calls so the UI card is not green
- orchestrator.py + agent.py: run_stream(hidden=) for form submissions
(single is_display=False, is_context=True user message)
- api/websocket.py: extract _start_agent_run helper, add form_submit
branch that delivers submitted form values as a hidden user message
- profiles/secretary: enable navi_ui "ui" group (agent + subagent)
- .env.example: NAVI_UI_MCP_* flags
- tests/unit/mcp/test_ui_server.py
Webclient
- components/ui/{registry.js,CardGrid.vue,Form.vue}: auto-discovered
renderers (snake_case <-> PascalCase aliasing)
- components/messages/UiComponentCard.vue: wrapper rendered in
AssistantMessage when entry.kind === 'ui_component'
- stores/chat.js: extract ui_component from tool_call metadata in both
live stream and history-replay paths
- composables/useWebSocket.js: note that ui_component rides tool_call
- tests/unit/components/ui/* (17 tests)
- dist rebuilt
Excluded from the vmkdemo port: the stale single-file navi/mcp/ui_server.py
duplicate, the realtor profile, and the vmk_data server (unrelated real
estate work that was interleaved with navi_ui on vmkdemo).
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 9 Jul
|