Ngent Server is a Go service that exposes HTTP/JSON APIs and SSE streaming for multi-client, multi-thread agent turns.
The system targets ACP-compatible agent providers, lazily resolves per-thread/session-scope providers, persists interaction history in SQLite, and bridges runtime permission requests back to clients.
Current built-in providers are codex, claude, cursor, opencode, gemini, kimi, qwen, and blackbox.
This file is the source of milestone progress, validation commands, and next actions.
Post-M8ACP multi-agent readiness and maintenance.
Post-M8Web UI markdown file preview drawer completed:- added thread-scoped local file preview endpoints:
GET /v1/threads/{threadId}/file-preview?path=...GET /v1/threads/{threadId}/file-preview-content?path=...
- backend preview now validates absolute file paths against configured allowed roots, resolves symlinks before authorization, and supports only text/image content types.
- text previews now render one bounded prefix with absolute line numbers: the backend reads at most the first 10000 lines and applies
#L<number>highlight only when that target line falls inside the returned range. - image previews stream the raw file bytes through the new content endpoint so the browser can render authenticated local images without exposing bearer tokens in query strings.
- finalized markdown links whose
hrefis an absolute local file path are now rendered in the Web UI as ordinary inline file links instead of plain browser navigation targets. - clicking those inline file links reuses the existing right-side drawer:
- text files show at most the first 10000 lines with optional focused-line highlight.
- image files show an inline preview surface.
- opening a message-linked file explicitly dismisses any currently open git-diff file drawer first.
- shared drawer follow-up polish: file-content previews now use a single line-number column instead of the diff view's two-column old/new gutter; the full two-column gutter remains only for tracked patch views.
- unsupported file types remain non-previewable from this surface and render as disabled inline links rather than broken browser navigations.
- follow-up polish:
- removed the message-link file icons so only the label and optional
L<number>badge remain in transcript markdown. - removed the preview drawer's file-type icon row for message-linked previews; git-diff file rows keep their existing icons.
- removed the message-link file icons so only the label and optional
- validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- added thread-scoped local file preview endpoints:
Post-M8session-history replay load-gating and history-event batching completed:- moved selected-session transcript live-load gating from the embedded SPA into the backend
/historypath. - session-scoped history now calls provider
session/loadonly when the selected session has no filtered turns in the current/historyresponse; once filtered turns exist,/historywill reuse cachedsessionTranscriptif available but will not live-load a missing transcript from the provider. - the embedded SPA keeps the simpler merge path: when replay is present it still renders replay first and local turns after it, but same-version backend behavior now determines whether replay comes from cache or from a fresh provider load.
- follow-up performance fix: thread history no longer issues one SQLite
ListEventsByTurnread per turn when events are needed; the handler now batch-loads events for the thread's turns in one storage call and groups them in memory before session filtering/serialization. - documentation now records the backend-gated live-replay policy and the remaining limitation that provider-only prehistory is unavailable after ngent turns exist unless a transcript snapshot was warmed into cache earlier, and that warmed transcript snapshots can still overlap with persisted turns because replay remains separate from
turns/events. - validation:
- pass:
go test ./... - pass:
git diff --check
- pass:
- moved selected-session transcript live-load gating from the embedded SPA into the backend
-
Post-M8memory-file redundancy cleanup completed:- kept
docs/SPEC.mdfocused on current-state behavior while retaining older rollout notes in the appendix instead of duplicating them in the normative sections. - kept
docs/DECISIONS.mdaligned with the canonical ADR set so superseded/current entries and later ADR numbering stay unambiguous. - split
docs/KNOWN_ISSUES.mdmaintenance into active and closed sections so closed entries no longer live partly in the active registry and partly in a tail section. - kept
docs/ACCEPTANCE.mdrequirement 31 aligned with the current append-only event storage plus read-time history compaction behavior. - consolidated repeated same-day
PROGRESS.mdheadings while preserving the underlying change notes. - validation:
- pass:
git diff --check - pass: memory-file structure review across
PROGRESS.md,docs/KNOWN_ISSUES.md,docs/SPEC.md,docs/DECISIONS.md, anddocs/ACCEPTANCE.md
- pass:
- kept
-
Post-M8Web UI git-diff drawer stability fix completed:- opening a changed file in the git-diff chip no longer installs outside-click or focus-leave auto-dismiss behavior, so the right-side drawer stays open during normal workspace interaction instead of collapsing on the next arbitrary click.
- the open drawer now renders from a browser-local preview snapshot keyed by thread session, while
/v1/threads/{threadId}/git-diffpolling continues to refresh only the chip and file list. - keeping the drawer off the poll-driven re-render path prevents forced detail reloads, DOM replacement flicker, and scroll/content jumping while the same file preview is open.
- follow-up fix: collapsing the git-diff chip no longer closes the right-side drawer; the drawer is now dismissed only by its own close button.
- follow-up fix: clicking a file row now always refetches that file's detail from the backend instead of reusing the prior open's cached drawer payload.
- follow-up polish: the drawer content rows no longer render horizontal separators, and the diff/content typography now matches the composer footer's model-label styling.
- follow-up fix: the drawer's
<code>content now explicitly inherits that model-label font family instead of falling back to the browser's default code font. - follow-up polish: the drawer content rows now use tighter line-height and vertical padding so each row is shorter without reducing text size.
- follow-up fix: tracked diff previews now derive real old/new line numbers from each hunk header, show both line-number columns in the drawer, keep hunk headers visible without numbers, and hide raw diff header metadata such as
diff --git/index/---/+++. - follow-up API compaction:
/v1/threads/{threadId}/git-diff-filenow returns grouped renderedblocks[]instead of duplicating rawcontentplus per-linelines[]; adjacent same-tone rows are merged into one block carryingtext[]and only the relevantoldLineNumbers[]/newLineNumbers[]. - follow-up simplification: the Web UI now renders those server-produced blocks directly and infers whether old/new line-number columns should appear from the presence of the corresponding number arrays, so the old
showLineNumbersflag is gone. - performance note: the structured diff blocks are still produced server-side in one linear scan over the already loaded file/diff text, without introducing extra git subprocess calls per request.
- session-scoped preview snapshots are preserved separately per selected session, so switching sessions hides unrelated previews and returning to the prior session restores that session's last opened drawer content.
- validation:
- pass:
cd internal/webui/web && npm run build
- pass:
-
Post-M8Web UI git-diff per-file preview drawer completed:- added
GET /v1/threads/{threadId}/git-diff-file?path=..., backed byinternal/gitutil.FileDetail, so the server can return per-file preview payloads for the currently visible working-tree diff. - tracked text changes now load raw patch content from
git --no-pager diff -- <path>, while untracked text files return their current file contents directly; binary/non-text files are marked non-viewable instead of opening a broken preview. GET /v1/threads/{threadId}/git-difffile rows now also exposeviewable, allowing the embedded SPA to disable unsupported rows up front.- the embedded Web UI git-diff chip still expands to the changed-file list, but clicking a viewable row now opens a right-side slide-out drawer over the chat pane, with close affordance, per-line diff/content rendering, and same-session file switching without leaving the current conversation.
- follow-up polish: untracked binary rows now still show the existing
Newbadge while remaining disabled, and the drawer keeps its selection/browser-local open state scoped to the active thread session instead of leaking into backend state. - validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- added
-
Post-M8grouped thread-session collapse affordance completed:- the embedded Web UI grouped left rail now lets each thread collapse or expand its own inline session list without affecting other threads.
- the leading agent glyph now doubles as the collapse toggle:
- expanded groups keep the provider avatar at rest and switch to a down-chevron affordance on hover/focus.
- collapsed groups keep a right-chevron visible so hidden sessions remain discoverable.
- toggling collapse preserves the existing thread activation flow, overflow menu, and
New sessionaction; starting a new session auto-expands that thread again so the fresh session row can appear immediately. - no backend/API/runtime behavior changed; this is a browser-local Web UI interaction update layered on top of the existing grouped rail.
- validation:
- pass:
cd internal/webui/web && npm run build
- pass:
-
Post-M8cross-client active session spinner visibility completed:GET /v1/threads/{threadId}/sessionsnow decorates each returned session row withisActivewhen that concrete(thread, session)scope currently owns a live turn.- the response still prepends the thread's currently bound
sessionId, so even if the upstream provider session catalog is stale, a newly opened browser can still see the active session row and its loading spinner. - the embedded Web UI now merges server-reported
session.isActivewith its existing local in-browserstreamStates, so the grouped left rail shows the same spinner both for turns started in the current browser and for sessions another browser opens while already active. - targeted HTTP coverage now verifies that a second client querying
/sessionsduring a running bound-session turn seesisActive=trueon the active session row and that the flag clears again after completion. - validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
-
Post-M8Web UI merged thread/session left rail completed:- removed the dedicated middle session drawer and its chat-edge collapse toggle; the workspace now uses one left navigation column plus the main chat area.
- the left rail now renders each thread as a grouped header with its ACP session rows directly underneath, closer to Codex App's project/session browsing model while preserving the existing thread-based backend model.
- session switching now activates the clicked thread and selected session in one step, so choosing a historical session from another thread opens the correct transcript immediately instead of briefly rendering that thread's default session first.
- each thread group now shows at most 5 sessions at first render;
Show moreexpands the next chunk and still honors backendnextCursorpagination when additional provider pages exist. - per-thread
New session, refresh, loading indicators, session-title live updates, and post-turn session-list refresh behavior were all retained inside the merged grouped rail. - follow-up polish: thread headers no longer show relative timestamps or any selected/highlighted state; only session rows carry selection styling.
- follow-up polish: session refresh moved out of the inline header controls into the thread three-dot overflow menu, and the grouped thread header now uses the agent/provider icon as its leading visual instead of a generic folder glyph.
- follow-up polish: thread-group hover/background feedback was removed so only session rows react on hover, thread-header actions now stay visible without hover reveal, and inter-group spacing was tightened so the rail reads as one continuous list.
- follow-up polish: the thread overflow trigger is now permanently visible like
New session, active session rows use a stronger accent treatment for clearer selection, and the rail side padding was reduced so content sits closer to the panel edge. - follow-up polish: thread-header overflow buttons now react only to direct button hover instead of inheriting whole-row hover feedback, the rail padding/indent was tightened again, thread headers no longer show a streaming spinner for active sessions, and the active-session left accent line was softened.
- follow-up polish: the old chat-edge collapse handle pattern that previously controlled the session drawer now controls the merged threads rail, so the left thread/session panel can collapse and expand with the same hover-reveal affordance.
- validation:
- pass:
cd internal/webui/web && npm run build
- pass:
-
Post-M8Web UI live-plan bottom overlay completed:- ACP
sessionUpdate:"plan"updates no longer render inside the assistant transcript bubble or finalized history message. - the embedded Web UI now renders the current running turn's latest plan as an ephemeral bottom-floating card anchored above the chat input, so users can track step progress without scrolling back to the top of a long reply.
- the floating card is hydrated from the same persisted
plan_updateturn events already used for running-turn recovery, so a refreshed browser or another browser entering the same active session sees the current live plan and continues receiving updates through the resumed per-turn SSE stream. - when the turn completes, errors, or disconnect-finalizes, the live plan card disappears together with the turn's streaming-only runtime state instead of remaining in transcript history.
- the message list now reserves dynamic bottom inset space while the live plan card is visible, preventing the floating card from covering the latest transcript content or the scroll-to-bottom button.
- follow-up fix: history cache hydration no longer reattaches stale
planEntriesfrom already-rendered in-memory messages, which previously could resurrect the old top-of-message plan block after session switches or history reloads.
- ACP
Post-M8session-scoped Web UI git diff summary and file-change panel completed:- added
GET /v1/threads/{threadId}/git-diff, backed byinternal/gitutil, which parsesgit --no-pager diff --shortstat,git --no-pager diff --numstat, and untracked file paths fromgit ls-files --others --exclude-standard -zfor the thread working tree. - the endpoint is fail-soft like the existing git branch API:
- if
gitis unavailable, it returnsavailable=false. - if the thread
cwdis not inside a git repository, it returnsavailable=false.
- if
- the embedded Web UI now polls this endpoint every 15 seconds only when the active thread has a selected concrete session id, immediately refetches when the user switches sessions, and refreshes again after turn completion, but the API itself no longer requires or echoes
sessionId. - when tracked or untracked working-tree changes exist, the composer now shows a Kimi-style change-summary chip above the input; expanding it reveals the per-file rows and repository root, and untracked files render with a dedicated "New" badge.
- follow-up fix: the Web UI now preserves the backend
untrackedflag during API normalization, so untracked rows actually render as the dedicated "New" badge instead of falling back to "Changed". - follow-up fix: the chip now tracks pending self-toggles separately from polled diff data, so the trigger's own
focusoutduring DOM re-render no longer reopens the panel and repeated real clicks open/close it immediately. - follow-up polish: expanded git-diff file rows now show suffix/file-name based type icons sourced from a locally vendored subset of
file-icons/vscodefont assets, with theme-aware tinted icon tiles and a generic fallback for unknown file types. - clean repositories, non-git directories, and hosts without
gitshow no diff chip at all.
- added
-
Post-M8Web UI Spanish/French localization and multilingual README expansion completed:- expanded the embedded Web UI
languagepreference from two values to four:en,zh-CN,es, andfr. - browser-locale detection now maps the closest supported locale on first load:
zh-*browsers default tozh-CN.es-*browsers default toes.fr-*browsers default tofr.- all other locales default to
en.
- localized the client-owned SPA chrome and relative-time labels for Spanish and French, and added Settings language switches for all four supported UI languages.
- added root
README.es.mdandREADME.fr.md, and cross-linked all root README variants so the repository landing docs are available in four languages.
- expanded the embedded Web UI
-
Post-M8active-turn viewer disconnect decoupling and resumable shared live SSE completed:- server-side turn execution is no longer tied to the lifetime of the original
POST /v1/threads/{threadId}/turnsresponse, so browser refreshes and other viewer disconnects no longer cancel a healthy in-flight turn unless the user explicitly requests cancel. - added
GET /v1/turns/{turnId}/events?after=<seq>so a refreshed browser or a second browser can replay persisted turn events and then tail the same live turn stream from the last seen per-turn sequence. - permission decisions now emit persisted/live
permission_resolvedturn events, so one browser approving a request updates the other browsers watching the same active turn instead of leaving stale pending cards behind. - restored turn-event persistence to true append-only-on-write semantics, which keeps per-turn
seqordering stable for safe live resume while leaving any delta coalescing to read-time history serialization only. - thread list/get responses now expose
hasActiveSession, so a newly opened browser can immediately see which agent/thread currently owns a live session before opening that conversation. - the embedded Web UI now rehydrates running turns from persisted events on history load, restores the active session/live bubble/pending permissions after refresh, consumes
permission_resolved, fixes streaming-bubble dedupe during running-turn hydration, and reconnects transport-level viewer drops without converting them into user-visible turn cancellation.
- server-side turn execution is no longer tied to the lifetime of the original
-
Post-M8Web UI Simplified Chinese localization and Settings language switch completed:- added a browser-local persisted
languagepreference to the embedded Vite + TypeScript SPA, alongside the existing theme/auth/server settings. - first load now defaults to the closest supported browser locale:
zh-*browsers default tozh-CN.- all other locales default to
en.
- moved language switching into the Settings drawer and wired root-shell re-rendering so changing language updates the sidebar, session rail, composer, empty states, permission cards, markdown controls, and other primary UI chrome immediately.
- localized client-owned Web UI strings and relative-time labels for English + Simplified Chinese while keeping provider/server payload text as-is.
- added a browser-local persisted
-
Post-M8ACP session-usage persistence and Web UI context-pressure indicator completed:- added shared ACP session-usage handling for both
session/update {sessionUpdate:"usage_update"}and cumulative prompt-responseusagepayloads, including embedded Codex/Claude flows and shared ACP CLI drivers. - persisted per-session usage snapshots in sqlite
session_usage_cache, keyed by(agent_id, cwd, session_id)and merged withCOALESCEsemantics so partial provider updates can accumulate over time instead of overwriting earlier fields with nulls. - added
GET /v1/threads/{threadId}/session-usage?sessionId=...plus streamed/persistedsession_usage_updateturn events, so both the browser and later tooling can re-query usage after the original turn is gone. - the embedded Web UI now listens for live
session_usage_update, loads cached usage when a session is opened, and renders a small ring-only circular progress badge at the composer footer bottom-right to the right of the git branch pill. - the badge is intentionally fail-soft:
- if a provider never returns usage, the Web UI shows nothing.
- if a provider returns token totals but no context-window
used/size, the data is still cached in sqlite but the Web UI stays hidden because no safe percentage can be computed.
- added shared ACP session-usage handling for both
-
Post-M8Web UI git-branch footer and checkout switcher completed:- added thread-scoped git inspection/switch support through new
GET/POST /v1/threads/{threadId}/gitendpoints backed byinternal/gitutil. - the backend now treats branch checkout as a thread-wide shared-state mutation: if any session on the thread is actively streaming, branch switching returns
409 CONFLICT. - missing
gitbinaries and non-repository working directories are handled as optional capability absence, so the API returnsavailable=falseand the frontend hides the control instead of surfacing a hard error. - the embedded Web UI composer now shows a Codex-style branch pill at the bottom-right only when the active thread's
cwdis inside a local git repository; expanding it reveals local branches and clicking a branch checks it out immediately. - after each turn settles, the Web UI refreshes git state so agent-driven branch changes are reflected without requiring a full page reload.
- added thread-scoped git inspection/switch support through new
-
Post-M8restrained desktop-workbench Web UI redesign completed:- Phase 0 baseline completed:
- reviewed the embedded SPA implementation and captured before screenshots for the home empty state,
New Agentmodal, and chat workspace.
- reviewed the embedded SPA implementation and captured before screenshots for the home empty state,
- Phase 1-2 visual foundation and shell completed:
- replaced the earlier glassy/ascii-branded presentation with a denser desktop-tool direction built on cooler neutral surfaces, restrained blue emphasis, tighter radii, flatter shadows, and a local/system font stack.
- removed decorative background treatments and rebuilt the shell so the main chat workspace is visually primary while the left rails recede.
- redesigned the no-thread workspace into an anchored workbench overview panel with one primary action instead of a large floating empty state.
- Phase 3-5 navigation, creation flow, and composer completed:
- converted thread and session browsing into more compact tool-style lists with lighter hover feedback and a restrained active-state marker instead of card-like highlight blocks.
- rebuilt the
New Agentmodal around a working-directory-first flow; agent selection now reads like a mature runtime picker and advanced JSON options are visually demoted. - compressed the chat header hierarchy and turned the composer into a cleaner editor-like control surface with secondary model/reasoning controls and a flatter send action.
- Phase 6-7 message system, dark mode, and responsive refinement completed:
- unified assistant content, reasoning, plan, tool calls, markdown blocks, and permission review under one document-like section language rather than unrelated plugin-card styles.
- tuned dark mode independently instead of relying on a simple inversion, and kept the mobile layout structurally coherent when the side rails collapse.
- behavior compatibility:
- kept the existing no-framework Vite + TypeScript SPA architecture, HTTP/JSON API usage, POST SSE stream path,
activeStreamMsgIdsentinel semantics, history-load guards, permission workflow,bindMarkdownControls(...)calls, and existing thread/session/input/cancel bindings intact.
- kept the existing no-framework Vite + TypeScript SPA architecture, HTTP/JSON API usage, POST SSE stream path,
- Phase 0 baseline completed:
Post-M8shared-browser thread/session visibility completed:- changed thread list/get/update/delete and other thread-scoped APIs to resolve threads globally by
threadIdinstead of requiring the creating browser'sX-Client-ID. - stopped binding runtime permission decisions and persisted attachment fetches to the original browser-local
clientId, so another browser can continue the same thread and open the same uploads. - removed the obsolete sqlite
threads.client_idfield and dropped the oldclientstable;X-Client-IDremains only as a required compatibility header and is no longer persisted. - changed recent-directory suggestions to use one global recency list instead of browser-scoped filtering.
- updated the Web UI attachment URL builder to stop sending
client_idon/attachments/*. - removed the Web UI's visible Client ID settings entirely; the browser now sends one fixed compatibility
X-Client-IDheader internally instead of exposing per-browser identity controls.
- changed thread list/get/update/delete and other thread-scoped APIs to resolve threads globally by
-
Post-M8Web UI fresh-session binding/render preservation fix completed:- when a fresh session receives
session_bound, the left session panel now upserts that boundsessionIdimmediately and forces a panel refresh instead of waiting for the first turn to finish and refresh the selected-session replay view. - session-panel dedupe now preserves richer later metadata (
title,cwd,updatedAt) for duplicate session ids, so temporary placeholders do not block later server-provided session details. - history reload now recognizes fresh-session scope promotion into a bound session and reuses the in-memory message cache for that first replay pass, preventing immediate transcript/history refresh from stripping streamed
thinking/tool_callsections off the just-finished first reply. - full-page refresh is also preserved now: when transcript replay overlaps with persisted turn history, the Web UI rehydrates the replayed messages from the richer event-backed local turn reconstruction so
reasoning/tool_callsections survive reload. - backend
GET /v1/threads/{threadId}/sessionsnow also prepends the thread's currently boundsessionId, so the active session remains visible even when the upstream providersession/listresult is stale or has not surfaced that new session yet.
- when a fresh session receives
-
Post-M8initial Web UI visual refresh completed (later superseded by the 2026-03-30 desktop-workbench redesign):- kept the existing no-framework SPA data flow, SSE behavior, store semantics, and API contracts unchanged; the change set is UI-only.
- refreshed the shell, navigation, composer, overlays, and permission surfaces as an earlier quality pass before the later restrained workbench redesign.
- upgraded interaction polish across thread rows, session cards, slash-command popover, settings drawer, new-agent modal, permission cards, and attachment chips.
- updated the visual token system while keeping light/dark themes and responsive behavior intact.
- follow-up: reduced the chat-header session title size on both desktop and narrow/mobile breakpoints so long titles feel less heavy and consume less vertical attention.
- follow-up: switched sidebar/chat provider avatars from inline
<img>tags to CSS background-image icons so session switches andNew sessionresets no longer trigger repeat icon fetches for Codex avatars.
-
Post-M8Web UI user-message base64 image placeholder rendering completed:- user prompt bubbles now detect inline placeholders that start with
[Image: data:image/...;base64,...]and render them as immediate inline image previews instead of raw base64 text. - the parser is fail-soft and image-only: it accepts only
data:image/*;base64,...payloads, strips incidental whitespace inside the data URL, and falls back to ordinary markdown rendering for malformed or non-image placeholders. - ordinary user markdown/text rendering remains unchanged around those placeholders, and the message copy action still preserves the original raw message text.
- user prompt bubbles now detect inline placeholders that start with
-
Post-M8human-readable stderr logger rollout completed:- replaced the previous
slogJSON logger with a repo-local leveled logger ininternal/observability, keeping the existingDebug/Info/Warn/Errorcall shape while switching output to readable text lines onstderr. - changed HTTP request completion logs to a compact nginx-style access-log format such as
INFO: 2026-03-23 15:30:45 127.0.0.1 - "GET /v1/threads HTTP/1.1" 200 OK 12.4ms. - kept ACP
--debugtracing and secret redaction intact; debug traces now render as readable text with sanitized embedded JSON payloads instead of JSON log envelopes. - enabled ANSI color for level/status segments only when
stderris a TTY, so redirected output remains plain text.
- replaced the previous
-
Post-M8Cursor CLI ACP integration completed:- added
internal/agents/cursoron top of the sharedacpclidriver, with startup fallback acrossagent acpandcursor-agent acp. - wired
cursorinto startup preflight,/v1/agents, thread allowlist, turn factory, and agent-model discovery flow. - verified against the local installed Cursor CLI and the official ACP docs that Cursor requires an ACP
authenticatecall withmethodId="cursor_login"afterinitialize; without that step, realsession/newdoes not respond. - confirmed by local probing that Cursor model selection is exposed through ACP
configOptionsand must be applied viasession/set_config_option("model", ...);session/new.modelandsession/new.modelIdare ignored by the real CLI. - normalized generic ACP permission-option matching so providers advertising hyphenated kinds such as
allow-once/reject-oncestill flow through ngent's fail-closed default decision path. - added fake-process coverage for:
- authenticated ACP stream startup.
- model selection through
session/set_config_option. - model discovery after ACP authentication.
- session transcript replay after ACP authentication.
- validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./internal/agents/cursor ./internal/agents/acpcli ./cmd/ngent - pass:
go test ./...
- pass:
- added
-
Post-M8ACP assistant image/resource content rendering completed:- extended shared ACP
agent_message_chunkparsing so non-text assistant payloads are preserved as structured content blocks instead of being dropped when they are not{type:"text",text:...}deltas. - added a first-class assistant message-content callback in the agent layer and bridged those blocks through HTTP/SSE/history as
message_contentevents alongside the existingmessage_deltatext stream. - updated the Web UI assistant segment timeline to render
message_contentblocks during streaming and after reload/history reconstruction, with dedicated cards for image content and embedded resource content plus JSON fallback for unknown block shapes. - kept
responseTextas the visible-text aggregate only, so existing API consumers remain compatible while structured content survives in turn events.
- extended shared ACP
-
Post-M8BLACKBOX Web UI tool-call rendering normalization completed:- dropped standalone whitespace-only assistant chunks when they arrive as isolated separators around tool calls, preventing BLACKBOX turns from rendering large blank answer blocks or effectively empty assistant messages.
- widened Web UI tool-call payload handling so
content/locationsno longer require array-only shapes; single-object payloads are normalized and still rendered. - improved tool-call titles for generic BLACKBOX payloads such as search calls titled only
.by falling back to kind/path-derived labels.
-
Post-M8BLACKBOX AI ACP integration completed:- added
internal/agents/blackboxon top of the sharedacpclidriver, usingblackbox --experimental-acpwith stdout-noise tolerance because the CLI can emit non-JSON process/telemetry lines before or between ACP frames. - wired
blackboxinto startup preflight,/v1/agents, thread allowlist, turn factory, and agent-model discovery flow. - confirmed against local
blackbox 1.2.47ACP probing that:initializeandsession/newsucceed with the standard ACP handshake.- current initialize capabilities advertise
loadSession=false. - real
session/loadreturns-32601 method not found, so existing-session replay and session sidebar resume remain unavailable for now. - ACP startup itself does not require explicitly passing
BLACKBOX_API_KEYas long as the local BLACKBOX CLI already has a usable auth method configured; actual prompt execution still depends on valid upstream auth/network state.
- added fake-process coverage for:
- streaming with noisy stdout.
- model forwarding through startup args plus ACP
session/new/session/prompt. - fail-closed structured permission bridging.
- unsupported
session/listbehavior whenloadSession=false.
- added
-
Post-M8Web UI agent-list overflow trigger hover-only reveal completed:- changed the agent-list three-dot trigger to stay hidden by default and reveal only on row hover, keyboard focus, or while its menu is open.
- increased the trigger size slightly while keeping its hover surface compact, so the control is easier to hit once revealed without dominating the action column.
- validation:
- pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./...
- pass:
Post-M8Web UI left-rail layout alignment completed:- kept the agent/thread rail permanently expanded after follow-up product review; only the session panel now supports collapse/expand.
- removed the agent-list search box and its keyboard shortcut/filtering path; the left rail is now a straight thread list with actions only.
- moved
New agentbelow the agent list and updated the expanded session header to show the active agent name, project path, and a full-widthNew sessionentry above the session list. - removed the redundant uppercase
SESSIONSsection label above the session list after the new header/new-session block, keeping the panel header visually quieter. - restored the left-top brand label from
Agentsto the product nameNgent. - hid the session panel entirely until an agent/thread is selected, so first load no longer reserves a blank middle column.
- fixed the CSS visibility rule so the hidden session panel truly leaves the flex layout instead of still occupying width via
.session-sidebar { display:flex }. - flattened the expanded session panel surface so its header and list share the same background plane instead of reading as two differently colored sections.
-
Post-M8ACP tool-call streaming completed:- extended shared ACP
session/updateparsing to preserve structuredtool_callandtool_call_updatepayloads, includingtoolCallId, status/title/kind, content blocks, locations, and raw input/output payloads. - added first-class turn callbacks plus HTTP/SSE/history persistence for those tool-call events instead of dropping them at the provider boundary.
- updated the Web UI stream state and history reconstruction to merge tool-call events by
toolCallIdand render live/persisted tool-call cards alongside plan/reasoning/message output.
- extended shared ACP
-
Post-M8Web UI fresh-session reset fix completed:- explicit
New sessionnow allocates a client-side fresh-session scope even when the active thread already has no persistedsessionId, so repeatedNew sessionclicks no longer reuse the same anonymous chat buffer. - empty-session replay reconstruction now drops cancelled turns that never emitted
session_boundand never produced visible response text, preventing stale cancelled placeholders from reappearing after reload.
- explicit
-
Post-M8deferred thread config apply completed:- changed
POST /v1/threads/{threadId}/config-optionsto validate against available config options and persist threadagentOptions.modelId/agentOptions.configOverrideswithout mutating the live provider. - narrowed cached provider scope from full
agentOptionsto thread + session/fresh-session identity, so picker edits no longer evict the current session provider by themselves. - added turn-start config sync: right before streaming a new turn, ngent compares persisted thread selections against the cached provider's current model/reasoning state and only then applies changed options.
- updated acceptance/spec/ADR docs to describe the new "persist now, apply on next turn" behavior.
- changed
-
Post-M8Web UI thinking tense alignment completed:- kept the live reasoning toggle label as
Thinkingwhile deltas are still streaming. - switched finalized reasoning labels to
Thoughtso completed content reads in the past tense.
- kept the live reasoning toggle label as
-
Post-M8Web UI thinking markdown rendering completed:- switched finalized
Thinkingcontent from escaped plain text to the same sanitized markdown renderer used by finalized assistant replies. - kept streaming reasoning as plain text so partial markdown does not reflow while deltas are still arriving.
- extended markdown typography/code styles to apply inside expanded
Thinkingcontent as well as normal assistant message bubbles.
- switched finalized
-
Post-M8Web UI thinking fold UX completed:- changed the
Thinkingsection to a native collapsible panel in the Web UI. - keep in-flight reasoning panels expanded while
reasoning_deltais still streaming. - once the turn settles into persisted history, render the same
Thinkingcontent collapsed by default so final replies stay compact. - preserve manual expand/collapse state for finalized messages across in-page list re-renders.
- changed the
-
Post-M8Web UI thinking/reasoning visibility completed:- added a per-turn reasoning callback bridge in
internal/agentsso ACPthought_message_chunk/agent_thought_chunkupdates no longer stop at provider parsing. - persisted hidden reasoning as first-class
reasoning_deltaturn events in the HTTP/SSE layer, alongside existingmessage_deltaandplan_updateevents. - updated the Web UI stream state and history reconstruction so agent messages render a visible
Thinkingsection during streaming and after reload/history fetch. - kept assistant answer text as
responseText; reasoning remains event-backed instead of being merged into the visible answer body. - added regression coverage for:
- ACP notification routing of thought chunks into reasoning callbacks.
- SSE/history persistence of
reasoning_deltaevents.
- added a per-turn reasoning callback bridge in
-
Post-M8shared ACP CLI provider driver completed:- extracted
internal/agents/acpclias the shared ACP CLI lifecycle driver forqwen,opencode,gemini, andkimi, covering sharedinitialize/session/new/session/load/session/list/session/prompt/session/set_config_optionflow plus model discovery and transcript replay. - migrated the four ACP CLI providers to provider-hook configuration instead of maintaining separate copies of the same stdio/session orchestration logic.
- extended
internal/agents/acpstdiowith opt-in stdout-noise tolerance so Gemini can reuse the same transport instead of a provider-local JSON-RPC implementation. - preserved provider-specific behavior behind hooks:
geminikeeps temporaryGEMINI_CLI_HOMEbootstrapping and stdout noise filtering.qwen/kimikeep selectable permission-option mapping and fail-closed timeout handling.opencodekeeps synchronoussession/cancelbehavior.- pass: real host smoke
E2E_QWEN=1 go test ./internal/agents/qwen -run TestQwenE2ESmoke -count=1 -v -timeout 180s - pass: real host replay
E2E_QWEN=1 go test ./internal/agents/qwen -run TestQwenE2ESessionTranscriptReplay -count=1 -v -timeout 240s - pass: real host config probe
E2E_KIMI=1 go test ./internal/agents/kimi -run TestKimiConfigOptionsE2EDoesNotCreateSession -count=1 -v -timeout 240s - pass: real host smoke
E2E_KIMI=1 go test ./internal/agents/kimi -run TestKimiE2ESmoke -count=1 -v -timeout 180s - observed failure: real host smoke
E2E_OPENCODE=1 go test ./internal/agents/opencode -run TestOpenCodeE2ESmoke -count=1 -v -timeout 180sreturnedopencode: session/new: context deadline exceeded; tracked indocs/KNOWN_ISSUES.md.
- extracted
-
Post-M8Web UI/session reset regression fixed:- completed the
New sessionfix after switching to a historical ACP session: clearingthread.agentOptions.sessionIdstill evicts stale empty-scope providers, and now also marks the next turn as an explicit fresh-session request so prompt building skips[Conversation Summary]/[Recent Turns]injection for that first turn. - kept the fresh-session marker server-internal: it is persisted in
thread.agent_options_jsononly until the nextsession_bound, but stripped from public thread responses so the API contract stays stable. - added a backend regression test that reproduces the real
A -> historical B -> New session -> sendpath and verifies the new session transcript contains only the new user/assistant exchange instead of wrapped prior thread history. - disabled composer send/input while the Web UI is switching thread sessions, preventing users from submitting a turn against a session selection that is still in flight.
- validation:
- pass:
go test ./internal/httpapi -run 'Test(NewSessionResetSkipsContextInjection|TurnSessionBoundPersistsSessionIDAndSkipsContextInjection|UpdateThreadClearingSessionDropsStaleUnboundProvider)' -count=1 - pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- completed the
-
Post-M8session transcript replay sqlite cache completed:- added SQLite table
session_transcript_cachekeyed by(agent_id, cwd, session_id)to persist provider-owned session replay snapshots separately from hubturns/events. - session transcript replay now reads sqlite first; on cache miss it still calls provider
LoadSessionTranscript, then persists the normalized replay snapshot for later reuse. - cached session transcript snapshots survive server restart, so revisiting the same historical session no longer requires a fresh provider
session/loadevery time. - known tradeoff: cache freshness is not yet invalidated by provider
updatedAtmetadata; stale replay risk is tracked indocs/KNOWN_ISSUES.md. - validation:
- pass:
go test ./internal/storage ./internal/httpapi -run 'Test(SessionTranscriptCacheCRUD|ThreadHistoryEndpointIncludesSessionTranscriptForSelectedSession|ThreadHistoryEndpointUsesCachedSessionTranscriptAcrossRestart)$' -count=1 - pass:
go test ./...
- pass:
- added SQLite table
-
Post-M8ACPsession/loadtranscript replay standardization completed:- removed provider-specific transcript reconstruction for
kimi,opencode,codex, andqwen; provider transcript replay now uses the same ACP path for all four providers:- resolve the selected session from ACP
session/list - call ACP
session/load - collect replayed
session/updatenotifications intouser/assistantmessages
- resolve the selected session from ACP
- extended shared ACP update parsing to support standard
user_message_chunkandagent_message_chunkreplay events, and added a shared transcript collector forsession/loadhistory replay. - fixed Codex session replay to list and load within the same embedded runtime because Codex raw ACP
sessionIdvalues are runtime-scoped; cross-runtime reuse ofsession-1-style ids returnsunknown session. - real regression on rebuilt ngent:
kimi: ACPsession/loadsucceeds for historical sessions but Kimi CLI 1.20.0 emits no replaysession/updatenotifications, so transcript replay currently returnssupported=truewith an empty message list under the standard ACP-only implementation.qwen: a real locally-created Qwen session now replays transcript messages through the same ACPsession/list+session/loadpath; env-gated provider E2E confirms the replay contains the unique prompt marker from the created session.
- validation:
- pass:
go test ./internal/agents/qwen -count=1 - pass:
E2E_QWEN=1 go test ./internal/agents/qwen -run 'TestQwenE2E(Smoke|SessionTranscriptReplay)$' -count=1 -v -timeout 180s - pass:
cd internal/webui/web && npm run build - pass: real
opencodetranscript replay regression on rebuilt ngent - pass: real
codextranscript replay regression on rebuilt ngent - observed: real
kimisession/loadreturns no replay updates on Kimi CLI 1.20.0
- pass:
- removed provider-specific transcript reconstruction for
-
Post-M8Qwen session transcript replay fix completed:- direct ACP
session/loadfor that session replayeduser_message_chunkandagent_message_chunk, proving Qwen itself was returning transcript content. - ngent transcript replay initially failed with
503 UPSTREAM_UNAVAILABLEandqwen: session/load: qwen: connection closed. - root cause:
- ngent transcript replay parsing treated Qwen
tool_call_updatenotifications as fatal because theircontentpayload is an array/object, not the{type,text}shape used by text chunks. - the ACP stdio transport closes the whole connection when a notification handler returns an error, so one unexpected Qwen update aborted the replay before the RPC response arrived.
- ngent transcript replay parsing treated Qwen
- fix:
- relaxed shared ACP update parsing so only text-bearing
user_message_chunk/agent_message_chunknotifications are decoded strictly; non-text update payloads are ignored instead of aborting replay. - extended Qwen transcript fake-process coverage to include a
tool_call_updateregression payload.
- relaxed shared ACP update parsing so only text-bearing
- validation:
- pass:
go test ./internal/agents -run 'TestParseACPUpdate' -count=1 - pass:
go test ./internal/agents/qwen -run 'SessionTranscript' -count=1 - pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- direct ACP
Post-M8codex session identity and replay normalization completed:- fixed fresh Codex
New sessionpersistence so ngent no longer stores provisional runtime ids likesession-1as the thread session binding when a durable_meta.threadIdis not yet available. - deferred initial
session_boundpersistence/emission for fresh Codex sessions until a stable session id can be resolved after the first prompt, then updated in-memory and persisted threadagentOptions.sessionIdwith the durable id. - validation:
- pass:
go test ./internal/agents/codex -run 'Test(CodexShouldDeferInitialSessionBinding|NormalizeCodexSessionListResultUsesStableThreadID|CodexSessionMatchesIDAcceptsStableAndRawIDs|CodexStableSessionIDFallsBackToRawSessionID)$' -count=1
- pass:
- fixed fresh Codex
- Kimi CLI ACP integration completed:
- implemented
internal/agents/kimiwith one-turn ACP stdio lifecycle and fail-closed permission handling. - wired
kimiinto startup preflight,/v1/agents, thread allowlist, turn factory, model discovery, and startup config-catalog refresh. - added dual startup syntax fallback for current upstream docs drift: try
kimi acp, thenkimi --acpif ACP initialize closes immediately. - added fake-process provider tests, fallback coverage, and server/httpapi allowlist coverage.
- fixed Kimi thread model switching:
POST /v1/threads/{threadId}/config-optionswithconfigId=modelnow selects the target model via Kimi process startup--model, instead of assuming ACPsession/set_config_option(model)is implemented. - Kimi stream/config discovery paths now also pass the selected model through both process startup args and
session/newhints for compatibility.
- implemented
- Shared agent config/state refactor completed:
- extracted common built-in agent fields
Dir,ModelID, andConfigOverridesinto sharedinternal/agents/agentutil.Config. - extracted shared thread-safe mutable agent state into
internal/agents/agentutil.State. - migrated
gemini,opencode,qwen,kimi,codex, andclaudeto reuse the shared state helper instead of keeping duplicated per-provider copies of model/config override logic. - kept protocol/runtime behavior provider-specific; only constructor validation and common mutable state handling were unified.
- extracted common built-in agent fields
- Web UI Kimi icon completed:
- wired
kimiavatar rendering in the Web UI to use the new asset with the existing--containimage treatment. - fixed the remaining New Thread modal agent-card icon map so Kimi now renders consistently there as well.
- removed the forced white background from all
--containagent icons in message/thread views and from the modal's Kimi/OpenCode icon markup.
- wired
- validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
-
M0completed: repository memory files, architecture/spec docs, API/DB/context strategy, and compilable skeleton. -
M1completed:- implemented
GET /healthzandGET /v1/agents. - added optional bearer-token auth for
/v1/*via--auth-token. - enforced localhost default listen policy with explicit
--allow-public=truefor public interfaces. - added startup JSON log fields and unified JSON error envelope.
- implemented
-
M2completed:- implemented SQLite storage with
database/sql+modernc.org/sqlite. - added idempotent migration runner with
schema_migrationsversion tracking. - created tables/indexes for clients/threads/turns/events.
- implemented SQLite storage with
-
M3completed:- implemented thread create/list/get APIs.
- enforced
X-Client-ID, client upsert, allowlisted agents, and absolutecwdvalidation. - enforced cross-client thread access as
404.
-
M4completed:- implemented turn streaming endpoint:
POST /v1/threads/{threadId}/turnsreturning SSE. - introduced
internal/agentsstreaming interface andFakeAgent(10-50ms delta cadence). - added in-memory turn controller to enforce one active turn per thread.
- implemented cancel endpoint:
POST /v1/turns/{turnId}/cancel. - persisted every SSE event into
eventsand finalized turn with aggregatedresponse_text. - implemented history endpoint:
GET /v1/threads/{threadId}/historywith optionalincludeEvents=true. - added tests for SSE event sequence, history consistency, cancel behavior, and same-thread conflict (
409).
- implemented turn streaming endpoint:
-
M5completed:- added
internal/agents/acpstdio JSON-RPC provider with lifecycleinitialize -> session/new -> session/prompt. - added ACP inbound request handling for
session/request_permissionand method-not-found fallback for unknown requests. - added
testdata/fake_acp_agentdeterministic executable for permission bridge testing. - added permission bridge endpoint
POST /v1/permissions/{permissionId}and SSE eventpermission_required. - enforced fail-closed permission behavior on timeout/disconnect with consistent cancelled convergence in fake ACP flow.
- added M5 tests for permission_required event, approved continuation, timeout auto-deny, and SSE disconnect convergence.
- added
-
M6completed:- added runtime codex configuration flags
--codex-acp-go-binand--codex-acp-go-args. - updated
/v1/agentscodex status:unconfiguredwhen bin is absent,availablewhen configured. - switched turn execution to per-thread lazy provider resolution (
TurnAgentFactory), enabling codex ACP startup only when turn begins. - wired codex turns to
internal/agents/acpwith process working dir set tothread.cwd. - kept default test suite codex-independent and added optional env-gated codex smoke test (
E2E_CODEX=1,CODEX_ACP_GO_BIN). - added lazy startup test to verify provider factory is not called during thread creation.
- added runtime codex configuration flags
-
M7completed:- added context-window prompt injection from
threads.summary + recent non-internal turns + current input. - added runtime controls:
--context-recent-turns,--context-max-chars,--compact-max-chars. - added
turns.is_internalmigration and storage support for internal compact turns. - added
POST /v1/threads/{threadId}/compactto generate and persist rolling summaries. - updated history API default behavior to hide internal turns (
includeInternal=trueopt-in). - added tests for injected prompt visibility, compact summary effect, and restart recovery using shared SQLite file.
- added context-window prompt injection from
-
M8completed:- preserved one-active-turn-per-thread behavior (
409) and added parallel multi-thread test coverage. - added thread-agent idle TTL reclaim (
--agent-idle-ttl) with structured reclaim/close logs. - added graceful shutdown workflow with active-turn drain + timeout force-cancel logging.
- aligned API/SSE error code set to include
TIMEOUTandUPSTREAM_UNAVAILABLEwhile removing non-standard codes from baseline. - validated SSE disconnect fail-closed behavior and non-hanging turn convergence.
- updated acceptance checklist with executable
go testandcurlverification commands.
- preserved one-active-turn-per-thread behavior (
-
Post-M8maintenance completed:- finalized canonical Go module path as
github.com/beyond5959/ngent. - replaced placeholder import path references across in-repo Go sources/tests.
- finalized canonical Go module path as
-
Post-M8embedded codex migration completed:- switched codex provider from external
codex-acp-gopath-based process spawning to embeddedgithub.com/beyond5959/acp-adapter/pkg/codexacp. - removed user-facing codex binary path flags; codex runtime is now linked into server and lazily created per thread on first turn.
- kept HTTP API semantics unchanged (
threads/turns/sse/permissions/history) and preserved permission fail-closed round-trip. - updated
/v1/agentscodex status contract to runtime preflight-basedavailable|unavailable. - updated codex smoke test gate to
E2E_CODEX=1withoutCODEX_ACP_GO_BINpath dependency.
- switched codex provider from external
-
Post-M8real local codex regression completed:- fixed embedded runtime lifecycle bug in
internal/agents/codex/embedded.go(runtime.Start(context.Background())instead of timeout-bound context) and kept retry-on-turn-start-failure guard. - updated context composer so first turn with empty summary/history passes raw input, preserving slash-command semantics for embedded acp-adapter flows.
- executed real HTTP/SSE regression with required prompts plus same-thread conflict (
409), cancel convergence, and permission round-trip (approved+declined) using/mcp callon fresh threads.
- fixed embedded runtime lifecycle bug in
-
Post-M8docs refresh completed:- added root
README.mdin English with project goal,go installinstructions, and startup examples (local/public/auth) using default DB home$HOME/.ngent. - removed manual
mkdirsteps from README startup examples and documented that server auto-creates$HOME/.ngentfor default db path.
- added root
-
Post-M8db-path default improvement completed:- changed default
--db-pathfrom relative./ngent.dbto$HOME/.ngent/ngent.db. - added startup auto-create for db parent directory so users can run without explicitly passing
--db-path. - added unit tests for default path resolution and db parent directory creation.
- changed default
-
Post-M8cwd policy simplification completed:- removed runtime CLI parameter
--allowed-root. - server now allows user-specified absolute
cwdpaths by default. - updated docs and tests to reflect absolute-cwd policy.
- removed runtime CLI parameter
-
Post-M8docs framing update completed:- adjusted README/SPEC/API/ARCHITECTURE wording to emphasize ACP-compatible multi-agent goal.
- kept current-state note explicit: built-in providers are
codex,claude,opencode,gemini,kimi, andqwen. - simplified README startup path to
ngentwith explicitngent --helpguidance.
-
Post-M8startup log UX simplification completed:- replaced startup JSON line with multi-line human-readable stderr summary (QR code + port and URL hint).
- added per-request completion logs containing
req_time,method,path,ip,status,duration_ms, andresp_bytes. - normalized structured log
timeand request logreq_timeto UTCtime.DateTimeat second precision. - added unit test coverage for startup summary rendering and request completion log fields.
-
Post-M8LAN-friendly default bind completed:- changed default bind to
0.0.0.0:8686and--allow-publicdefault totrueso other devices can connect via the startup QR code.
- changed default bind to
-
Post-M8qwen ACP integration completed:- implemented
internal/agents/qwenwith one-turn process lifecycle (qwen --acp) and ACP flowinitialize -> session/new -> session/prompt. - implemented
session/updatedelta extraction (agent_message_chunk+content.text) andsession/request_permissionfail-closed mapping (approved/declined/cancelled). - wired qwen into main server startup preflight,
/v1/agentssupported list, thread agent allowlist, and turn agent factory. - added/updated tests for qwen provider and server wiring (
main+httpapiqwen allowlist coverage). - executed validation:
- pass:
qwen --version(0.11.0) - pass:
go test ./internal/agents/qwen -count=1 - pass:
go test ./cmd/ngent ./internal/httpapi -count=1 - pass:
E2E_QWEN=1 go test ./internal/agents/qwen -run TestQwenE2ESmoke -v -timeout 120s
- pass:
- implemented
-
Post-M8ACP stdio transport refactor completed:- extracted shared transport package
internal/agents/acpstdio(JSON-RPC stdio call/notify, inbound request handling, parse helpers, process termination helper). - refactored
internal/agents/opencodeandinternal/agents/qwento reuseacpstdiowhile preserving provider-specific behavior. - executed regression:
- pass:
go test ./internal/agents/opencode ./internal/agents/qwen -count=1 - pass:
go test ./... - pass:
E2E_OPENCODE=1 go test ./internal/agents/opencode -run TestOpenCodeE2ESmoke -v -timeout 90s - pass:
E2E_QWEN=1 go test ./internal/agents/qwen -run TestQwenE2ESmoke -v -timeout 120s
- pass:
- extracted shared transport package
-
F2completed:- created
src/types.ts: full TypeScript interface set (Thread, Turn, Message, PermissionRequest, StreamState, AppState, etc.). - created
src/utils.ts: generateUUID (crypto.randomUUID + fallback), formatTimestamp, formatRelativeTime, isAbsolutePath, escHtml, debounce. - created
src/store.ts: singleton AppStore with pub/sub (subscribe → unsubscribe fn) and localStorage persistence for browser settings. - created
src/components/settings-panel.ts: slide-in drawer for browser-local connection/auth/theme controls; settings persist immediately. - updated
src/main.ts: imports store for theme init, wires settings button → settingsPanel.open(), system-theme change listener. - added settings drawer CSS to style.css (overlay, slide-in animation, theme button group).
- implemented complete CSS design system with CSS custom properties for light/dark themes (
[data-theme]). - built two-column IM layout: 260px sidebar (brand, search, thread list, settings footer) + flex main chat area.
- thread list items with avatar, title, preview, agent badge, relative time, and unread dot.
- chat header with thread title, agent badge, cwd, compact/cancel action buttons.
- scrollable message list with user bubbles (right, blue) and agent bubbles (left, neutral), typing indicator, permission card, day divider.
- input area with auto-resize textarea, send button, and keyboard hint.
- empty state for no-thread-selected view.
- basic responsive: ≤768px sidebar hides, mobile menu toggle shows.
- system-theme detection on boot +
prefers-color-schemelistener for live updates. - initialized Vite + TypeScript frontend under
internal/webui/web/. - created
internal/webui/webui.gowith//go:embed web/distand SPA fallback handler. - registered
FrontendHandlerinhttpapi.Config; non-API paths served by frontend, API routes unaffected. - updated
cmd/ngent/main.goto passwebui.Handler()and print a startup QR code for opening the UI. - updated
Makefilewithbuild-web,buildtargets; updated.gitignorefornode_modules. go test ./...all green; end-to-end:GET /→ 200 HTML,/threadsSPA fallback → 200,/v1/agents→ JSON,/healthz→ JSON.- standardized
/v1/agentsdisplay names toCodexandClaude Code. - synchronized test fixtures and API documentation examples with the same canonical names.
/v1/agentsoutput uses display names (not lowercase ids):Codex,Claude Code.
- created
-
F3completed:- created
src/api.ts: ApiClient with ApiError class; getAgents(), getThreads(), createThread(); reads serverUrl/authToken from store on every request and sends the compatibility client header internally. - created
src/components/new-thread-modal.ts: centered modal with agent card grid (radio, disabled for unavailable), CWD absolute-path validation, optional title, collapsible JSON agent-options textarea, submit with spinner, error banner; targeted DOM updates avoid full re-render during typing. - appended modal/form/agent-grid/skeleton/spinner CSS to style.css.
- rewrote
src/main.ts: init() loads agents+threads in parallel, store.subscribe drives updateThreadList()+updateChatArea(); skeleton loading state; search filter; thread click sets activeThreadId; new-thread button opens modal; error banner on API failure. tsc -b && vite buildpasses;go test ./...all green.
- created
-
F4completed:- created
src/sse.ts:TurnStreamclass usingfetch+ReadableStream(notEventSource, which lacks POST/header support); parsesevent:/data:SSE lines delimited by\n\n; dispatchesonTurnStarted,onDelta,onCompleted,onError,onPermissionRequired,onDisconnectcallbacks; supportsabort()for clean cancellation. - added
startTurn(threadId, input, callbacks)andcancelTurn(turnId)toApiClientinsrc/api.ts. - rewrote
src/main.tswith full message rendering and smart subscribe handler:handleSend(): adds user message → setsactiveStreamMsgIdsentinel → sets store streamState → appends streaming bubble to DOM → starts SSE stream.onDelta: targeted DOM update on#bubble-<id>(no re-render, no flicker); removes typing indicator on first delta.onCompleted/onError/onDisconnect: clears stream tracking → adds finalized message to store → clears streamState; subscribe re-renders list with final message.- subscribe handler: full
updateChatArea()only on thread switch; skipsupdateMessageList()whileactiveStreamMsgIdis set (streaming bubble live in DOM);updateInputState()always. handleCancel(): sets store streamState tocancelling, callsapi.cancelTurn().- Cancel button in chat header: hidden by default, shown during streaming via
updateInputState().
- added
.chat-header-metastyle tostyle.css. tsc -b && vite buildpasses;go test ./...all green.
- created
-
F5completed:- added
api.getHistory(threadId)callingGET /v1/threads/{threadId}/history, returnsTurn[]. - added
turnsToMessages(turns): converts serverTurn[]to clientMessage[]; skipsisInternalturns; uses stable IDs${turnId}-u/${turnId}-a; mapscancelled/error/completedturn statuses toMessageStatus; falls backcompletedAt → createdAtfor timestamp. - added
loadHistory(threadId): async, loads history on thread switch; guards against stale response (navigated away / streaming in progress); updatesstore.messages[threadId]. - updated
updateChatArea(): shows cached messages immediately on revisit (no spinner flash); shows centered loading spinner for first visit; always firesloadHistory()in background to keep view fresh. - added
.message-list-loading+.loading-spinnerCSS (centered spinner, reuses@keyframes spin). tsc -b && vite buildpasses;go test ./...all green.
- added
-
F6completed:- created
src/components/permission-card.ts:mountPermissionCard(listEl, event)appends an inline permission card to the message list; 15s countdown timer withsetIntervalat 100ms ticks; Allow/Deny buttons callapi.resolvePermission(); 409 (already resolved) treated as silent success;showResolved()snaps progress bar width, recolors card header/border, replaces buttons with resolved label; timeout auto-transitions topermission-card--timeoutstate. - added
api.resolvePermission(permissionId, outcome)—POST /v1/permissions/{permissionId}. - wired
onPermissionRequiredcallback inhandleSend(): callsmountPermissionCard(listEl, event). - added CSS:
.permission-progress+.permission-progress-bar(width-animated,transition: width .1s linear);.permission-card--approved|declined|timeoutoutcome states (border + header recolor);.permission-resolved--approved|declined|timeoutlabel colors;.perm-avatarwarning-color avatar. tsc -b && vite buildpasses;go test ./...all green.
- created
-
F7completed:- created
src/markdown.ts: configuredmarked@^9with custom renderer;html()override returnsescHtml(text)(XSS protection);code()renderer emits.code-blockHTML with hljs syntax highlighting, header (lang label + Copy button),<pre>with<code class="hljs">, and optional "Show all N lines" expand button for blocks >20 lines; registered go/typescript/javascript/python/bash/json/yaml language subset. - exported
renderMarkdown(text)— callsmarked.parsesynchronously. - exported
bindMarkdownControls(container)— binds.code-copy-btn(clipboard + 2s "Copied ✓" feedback),.code-expand-btn(togglecode-pre--collapsed),.msg-copy-btn(full bubble text copy); idempotent viadata-boundguard. - updated
src/main.ts: importedrenderMarkdown+bindMarkdownControls;renderMessage()usesrenderMarkdown(bodyText)+message-bubble--mdclass for agentdonemessages; adds<button class="msg-copy-btn">⎘</button>in message-meta for done messages;updateMessageList()callsbindMarkdownControls(listEl)after HTML injection. - added CSS to
style.css:.message-bubble--md(white-space: normal; nested p/h1-h3/ul/ol/blockquote/inline-code/a styles);.message--agent .message-group { max-width: min(660px, 90%) }for code block width;.code-block,.code-block-header,.code-lang,.code-copy-btn(hover-visible, 2s "Copied ✓" variant);.code-pre,.code-pre--collapsed(340px max-height + gradient fade::after);.code-expand-btn(full-width, text-secondary);.msg-copy-btn(hover-visible on agent message hover); hljs CSS-variable token colour system with light/dark variants. tsc -b && vite buildpasses;go test ./...all green.
- created
-
F8completed:- added
isNearBottom(el)helper (100px threshold). - smart auto-scroll in
onDelta: only scrolls when user is already at/near bottom; lets user read history uninterrupted during streaming. - added scroll-to-bottom button (
.scroll-bottom-btn) positioned absolute in new.message-list-wrapcontainer: shown when user scrolls away from bottom, hidden onupdateMessageListand smooth-scroll on click. - added
bindScrollBottom(): scroll-event listener syncs button visibility; buttonclickcallslistEl.scrollTo({ behavior: 'smooth' }). - added
bindGlobalShortcuts()(called once frominit()):/key: focuses#search-inputwhen not already in an input (preventDefault).Cmd+N/Ctrl+N: opens new thread modal.Escape(contextual, most-specific first): (1) closes mobile sidebar if open, (2) clears + blurs search if search is focused, (3) cancels active stream viahandleCancel().
- mobile UX: thread click in
updateThreadList()now removessidebar--openclass so sidebar closes automatically on thread select. - input hint updated to reflect shortcuts:
⌘ Enter to send · Esc to cancel · / to search. - CSS:
.message-listwrapped in.message-list-wrap(flex:1; position:relative; min-height:0; overflow:hidden);.message-listchanged fromflex:1toheight:100%;.scroll-bottom-btncircular, shadow, hover translate. tsc -b && vite buildpasses;go test ./...all green.
- added
-
F9completed:- updated startup output to include a QR code with port hint (for LAN access) and updated
TestPrintStartupSummary. - updated
docs/API.md: added Frontend Web UI endpoint section (GET /returnstext/html,GET /assets/*serves static assets, SPA fallback for non-API paths). - updated
docs/ACCEPTANCE.md: added Requirement 13 (Embedded Web UI) withgo test ./internal/webuiverification command. - updated
README.md: added "Web UI" section with startup summary example, feature list (threads/streaming/permissions/history/themes), no-Node-at-runtime note, andmake build-web && go build ./...rebuild instructions. - verified ADR-018 (Embedded Web UI via Go embed) already present in
docs/DECISIONS.md. - final verification:
go test ./...all green;tsc -b && vite buildpasses;go build ./...succeeds.
- updated startup output to include a QR code with port hint (for LAN access) and updated
-
Optional enhancement 1: add embedded-runtime preflight diagnostics endpoint (auth, app-server reachability, version compatibility).
-
Optional enhancement 2: WebSocket streaming transport in addition to SSE.
-
Optional enhancement 3: History/event pagination and cursor-based replay.
-
Optional enhancement 4: RBAC and finer-grained authorization policies.
-
Optional enhancement 5: Expanded audit logs and retention tooling.
-
Optional enhancement 6: expose environment diagnostics for codex local state DB/schema mismatches (for example
~/.codex/state_5.sqlitemigration drift) and app-server method compatibility. -
Post-M8Claude Code embedded provider completed:- implemented
internal/agents/claude/embedded.gobacked bygithub.com/beyond5959/acp-adapter/pkg/claudeacp.EmbeddedRuntime. - preflight checks
ANTHROPIC_AUTH_TOKENenvironment variable; status reportsavailablewhen set,unavailableotherwise. ANTHROPIC_AUTH_TOKENandANTHROPIC_BASE_URLare read from environment viaclaudeacp.DefaultRuntimeConfig()at startup.
- implemented
-
Post-M8thread delete lifecycle completed:- added
DELETE /v1/threads/{threadId}with same-client ownership check and409 CONFLICTwhen a thread has an active turn. - guarded deletion with a temporary turn-controller lock to prevent new turns from starting during delete.
- implemented transactional storage deletion in
internal/storagewith dependent cleanup (events->turns->threads). - wired Web UI thread-list delete action with confirmation and local state cleanup (threads/messages/active selection/stream state).
- executed validation:
- added unit tests (
TestPreflight_*,TestNew_*,TestClose_*,TestDefaultRuntimeConfig_ReadsEnv) covering token presence/absence, default/custom timeouts, and idempotent close. - added optional real smoke test (
E2E_CLAUDE=1 go test ./internal/agents/claude/ -run TestClaudeE2ESmoke -v -timeout 120s); confirmedPONGresponse andstopReason=end_turn(16.68s). - wired claude into
cmd/ngent/main.go: preflight call,"claude"inAllowedAgentIDs,case "claude"inTurnAgentFactory, real status insupportedAgents. - updated
main_test.go:supportedAgentssignature extended withclaudeAvailable bool; added claude id/status assertions. - executed validation:
- pass:
go build ./... - pass:
go test ./...(all packages green) - pass:
E2E_CLAUDE=1 go test ./internal/agents/claude -run TestClaudeE2ESmoke -v -timeout 120s(response:PONG, stopReason:end_turn)
- pass:
- added
-
Post-F9CI and release pipeline completed:- removed
internal/webui/web/dist/andtsconfig.tsbuildinfofrom git tracking (git rm --cached); added both to.gitignore. - created
.github/workflows/ci.yml: triggers on every push/PR (non-tag); steps: Go + Node.js 20 setup,make build-web, gofmt check,go test ./.... - created
.github/workflows/release.yml: triggers onv*.*.*tags; runsgoreleaser release --cleanwithGITHUB_TOKEN. - created
.goreleaser.yml(version 2):before.hooks: make build-web; cross-compiles linux/darwin/windows × amd64/arm64 (windows arm64 excluded) withCGO_ENABLED=0; producesngent_VERSION_OS_ARCH.tar.gzarchives +checksums.txt. - updated
Makefile:make buildnow outputs tobin/ngent(wasgo build ./...). - updated
README.md: replacedgo installwith "Download pre-built binary" table + "Build from source" (make build) instructions. - updated
AGENTS.md: replaced "MUST keep web/dist committed" rule with "MUST NOT commit web/dist"; added CI/Release pipeline section.
- removed
- Date:
2026-02-28 - Commands executed:
gofmt -w $(find . -name '*.go' -type f)go test ./...tsc -b && vite build(frontend)go build ./...
- Result:
- formatting: pass
- tests: pass (all packages green)
- frontend build: pass (121 kB JS, 27 kB CSS)
- full binary build: pass
- Date:
2026-03-03 - Commands executed:
go test ./internal/agents/opencode ./internal/agents/qwen -count=1E2E_OPENCODE=1 go test ./internal/agents/opencode -run TestOpenCodeE2ESmoke -v -timeout 90sqwen --versiongo test ./internal/agents/qwen -count=1E2E_QWEN=1 go test ./internal/agents/qwen -run TestQwenE2ESmoke -v -timeout 120sgo test ./cmd/ngent ./internal/httpapi -count=1go test ./...
- Result:
- opencode/qwen package tests: pass
- opencode real smoke: pass (
PONG,stopReason=end_turn) - qwen version check: pass (
0.11.0) - qwen package tests: pass
- qwen real smoke: pass (
PONG,stopReason=end_turn) - server/httpapi regression tests: pass
- full repo tests: pass
- Scope: write mandatory memory documents and create compilable package layout.
- DoD:
- required root/docs files exist and are coherent.
go test ./...passes.make runstarts the placeholder server.
- Scope:
/healthz,/v1/agents, auth toggle, startup logs. - DoD:
- endpoints return stable JSON.
- startup log is concise and includes listen endpoint, db path, and supported agent statuses.
- tests cover happy path and invalid config.
- Scope: storage layer, schema migration runner, storage unit tests.
- DoD:
- clients/threads/turns/events tables created by migrations.
- CRUD coverage for core entities.
- restart can read persisted records.
- Scope: create/list/get thread APIs and validation.
- DoD:
- strict request validation (agent allowlist, cwd absolute path).
- API tests cover valid/invalid requests and multi-client isolation.
- Scope: create turns, stream SSE events, query history, cancel turn.
- DoD:
- one active turn per thread enforced.
- cancel path is observable quickly in stream and persisted state.
- tests cover stream ordering and conflict handling.
- Scope: ACP provider integration with fake acp agent and permission forwarding.
- DoD:
- permission requests are forwarded to client and block until decision.
- timeout or missing decision fails closed.
- tests cover allow/deny/timeout/cancel races.
- Scope: codex-acp-go provider wiring and optional integration tests.
- DoD:
- provider can be enabled by config.
- integration test is optional and skipped by default without env setup.
- Scope: summary plus recent turns policy, compact trigger, restart recovery.
- DoD:
- prompt construction follows documented budget policy.
- compaction updates durable summary.
- restart resumes with consistent context state.
- Scope: conflict strategy, TTL cleanup, graceful shutdown, acceptance alignment.
- DoD:
- clear behavior for concurrent operations and stale sessions.
- background cleanup does not break active threads.
- acceptance checklist fully green.
-
Canonical module path is now finalized as
github.com/beyond5959/ngent. -
All in-repo Go import paths were updated from placeholder path to canonical path.
-
Post-F9Web UI multi-thread streaming behavior fixed:- removed UI behavior that aborted an in-flight SSE stream when switching threads.
- changed client stream tracking from single global
streamStateto per-threadstreamStates. - maintained per-thread stream runtime maps (stream handle, delta buffer, start time), so background threads can keep streaming and finalize correctly.
- wired send/cancel/input disable logic to current thread only, enabling concurrent in-flight turns across different threads.
- executed validation:
-
Post-F9permission countdown updated to 2 hours:- changed server default permission timeout to
2 * time.Hour. - changed Web UI Permission Required countdown to 2 hours and display format to
H:MM:SS.
- changed server default permission timeout to
-
Post-F9permission card persistence across thread switch fixed:- stored pending permission requests per thread in Web UI runtime state.
- when switching back to a thread, pending Permission Required cards are re-mounted with original deadline.
- resolved/timeout outcomes remove pending records to avoid stale prompts.
- executed validation:
-
Post-F9thread model selection and switching completed:- added thread update API
PATCH /v1/threads/{threadId}(agentOptions-only payload) with ownership checks and active-turn conflict (409). - added model discovery API
GET /v1/agents/{agentId}/models(ACP handshake-backed). - storage layer now supports
UpdateThreadAgentOptionsand updates threadupdated_at. - successful thread option update closes cached per-thread provider, so next turn re-initializes with new model config.
- wired runtime model discovery into all providers:
codex/claude: ACP embeddedsession/new.configOptions.gemini/kimi/opencode/qwen: ACPsession/new.models.availableModels.
- wired
agentOptions.modelIdforwarding into all providers:- embedded
codex/claudenow passmodelin ACPsession/new. gemininow passesmodelinsession/newandsession/prompt.kimipassesmodelinsession/prompt, and usesmodel+modelIdduring config/model discovery handshakes.opencodepassesmodelIdinsession/prompt;qwenpassesmodelinsession/prompt.
- embedded
- Web UI updates:
- new-thread modal model selector removed (create flow keeps agent/cwd/title/advanced JSON only).
- active thread header switched from free-text model input to ACP-backed model dropdown + Apply action.
- model controls are disabled while model lists load and during streaming turns.
- executed validation:
- added thread update API
-
Post-F9thread session model config switched to ACPconfigOptions:- added thread-scoped config options APIs:
GET /v1/threads/{threadId}/config-optionsPOST /v1/threads/{threadId}/config-options
POSTpersists selected model/config state directly into sqlite thread metadata (no separate apply endpoint/action).- provider-side config option support added across all built-in agents so persisted thread selections can be synchronized at turn boundaries:
- embedded:
codex,claude(cached runtime session sync). - stdio:
opencode,qwen,gemini,kimi(per-turn ACP handshake plus persisted selection forwarding).
- embedded:
- Web UI changes:
- removed thread header
Applybutton. - model dropdown persists immediately on selection.
- model source switched from agent-level model catalog to thread-level
configOptions(category=model). - model option descriptions are rendered under the selector in the chat header.
- removed thread header
- thread metadata sync:
- successful model switch persists
agentOptions.modelIdfor thread continuity and restart recovery.
- successful model switch persists
- executed validation:
- added thread-scoped config options APIs:
-
Post-F9thread reasoning selector and config override persistence completed:- backend now persists non-model session config selections under
agentOptions.configOverrideswhenPOST /v1/threads/{threadId}/config-optionssucceeds. - provider factories now restore persisted config overrides on fresh embedded sessions and per-turn stdio handshakes, so reasoning-style settings survive across future turns and restarts.
- Web UI composer footer now renders both
ModelandReasoningcontrols; reasoning options refresh from the latest threadconfigOptionsafter model changes. - reasoning control remains model-specific and is disabled/updated in the same active-turn safety envelope as model switching.
- added coverage for config override persistence and thread agent-option parsing.
- executed validation:
- backend now persists non-model session config selections under
-
Post-F9shared agent config catalog caching completed:- Web UI no longer re-fetches thread config options when switching between threads that use the same agent and already have a cached agent config catalog.
- frontend now keeps:
- thread-scoped current config state cache.
- agent-scoped shared config catalog cache for model/reasoning option lists.
- same-agent threads reuse the same model/reasoning lists while keeping independent selected current values derived from each thread's persisted
agentOptions. - composer footer pills now show only the selected model/reasoning names, without leading
MODEL/REASONINGlabels. - executed validation:
- pass:
cd internal/webui/web && npm run build
- pass:
-
Post-F9codex model-discovery stability improvement:- replaced per-request codex model discovery runtime startup/shutdown with a shared discovery client in
internal/agents/codex/models.go. - behavior changes:
- repeated
GET /v1/agents/codex/modelsnow reuses initialized embedded runtime instead of spawning and closing app-server each time. - one automatic retry with fresh discovery client when cached client becomes unhealthy.
- explicit cleanup hook added in server shutdown (
codexagent.CloseDiscoveryClient()).
- repeated
- local validation:
- first call
GET /v1/agents/codex/modelstook ~16s (initial discovery). - second call returned in ~0ms from shared client (no repeated startup/shutdown).
- first call
- replaced per-request codex model discovery runtime startup/shutdown with a shared discovery client in
-
Post-F9persisted agent config catalog completed:- added sqlite-backed
agent_config_catalogsstorage keyed byagent_id + model_id, with a reserved default snapshot row used when a thread has no explicit model selection yet. GET /v1/threads/{threadId}/config-optionsandGET /v1/agents/{agentId}/modelsnow read persisted catalog data first, so service restart can keep serving model/reasoning metadata without blocking on live provider queries.POST /v1/threads/{threadId}/config-optionsnow writes through both:- thread-local selected values into
threads.agent_options_json - current model config catalog into sqlite for reuse across threads/restarts
- thread-local selected values into
- service startup now launches a background catalog refresher that silently re-queries built-in agents and refreshes stored model/reasoning catalogs without delaying frontend availability.
- Web UI config cache is now keyed by
agent + selected model, so different threads on the same agent no longer accidentally reuse the wrong reasoning list for another model.
- added sqlite-backed
-
Post-F9streaming bubble typing-indicator persistence completed:- Web UI streaming agent bubble now keeps the three animated dots rendered at the bottom of the bubble after the first delta arrives, until the turn finishes.
- refactored transient streaming bubble DOM to use separate text and indicator regions so incremental
onDeltaupdates no longer replace the indicator.
-
Post-F9Web UI clipboard fallback and message-copy fix completed:- unified copy actions behind a best-effort clipboard helper that falls back to
document.execCommand('copy')whennavigator.clipboardis unavailable or blocked on LAN HTTP origins. - message copy buttons now copy the original message text payload instead of scraping rendered DOM text, so agent markdown replies no longer pull in code-block chrome such as
Copylabels. - applied the same fallback path to code-block copy and settings-panel client-id copy to keep behavior consistent across the UI.
- unified copy actions behind a best-effort clipboard helper that falls back to
-
Post-F9streaming bubble empty-line removal:- removed the blank spacer above the three animated dots before the first token arrives by hiding the empty text container in streaming bubbles.
- typing indicator now sits directly under the top padding until real content starts streaming.
-
Post-F9sidebar thread activity indicators completed:- thread list now shows a live spinner for any thread with an in-flight turn, so background work stays visible after switching to another thread.
- when a background turn finishes, the spinner flips to a green check badge that stays on that thread until the user opens it again.
- slowed the sidebar thread spinner slightly so the activity indicator reads as background work instead of a high-frequency busy loop.
-
Post-F9sidebar thread drawer actions and rename completed:- replaced the direct delete icon in sidebar thread rows with a drawer trigger.
- added drawer actions for inline rename and delete, with rename ordered before delete and delete styled as the only dangerous text action.
- extended
PATCH /v1/threads/{threadId}so thread title updates reuse the existing ownership and active-turn conflict model.
-
Post-F9sidebar thread action popover refinement completed:- replaced the expanding inline drawer with a floating popover anchored to the three-dot trigger, so opening thread actions no longer changes sidebar row height.
- kept rename and delete in the same order, with rename editing rendered in a floating panel and delete remaining the red dangerous action.
- executed validation:
-
Post-F9ACP plan streaming in Web UI completed:- added shared ACP
session/updateparsing for bothagent_message_chunkandplan, with plan routed through a new per-turnPlanHandlercontext callback. - introduced SSE/history event
plan_updateso ACP plans are persisted alongside other turn events instead of being dropped at provider boundaries. - updated the Web UI to render live plan cards during streaming and restore the latest plan from turn history on reload.
- added shared ACP
-
2026-03-06: Removed the thread action trigger's per-thread actionLabel/title tooltip in the Web UI popover menu; the three-dot button now uses a neutral
aria-labelonly, without hover text tied to the thread title. -
2026-03-06: Moved the sidebar thread action menu and rename form into a sidebar-level floating layer instead of rendering them inside each thread row, so the rename UI is no longer clipped by the thread list or sidebar overflow.
-
2026-03-06: fixed embedded codex permission bridge timeout mismatch by aligning adapter-side
session/request_permissionwait window to 2h (was 30s), matching hub default timeout and avoiding premature fail-closed during manual approval. -
2026-03-06: fixed embedded codex server-request compatibility for tool interaction:
item/tool/requestUserInputnow returns schema-compatible answers (auto-select first option label per question) instead of-32000 not supported.item/tool/callnow returns structured tool failure payload (success=false) instead of JSON-RPC method error, so app-server no longer aborts the whole flow on this request type.
-
2026-03-09: unified ACP message-chunk constant usage across stdio providers by removing per-provider
updateTypeMessageChunkdefinitions and reusingagents.ACPUpdateTypeMessageChunk. -
2026-03-09: hid the Web UI Reasoning switch when the active agent exposes fewer than two reasoning choices, so agents without switchable reasoning no longer show a dead control.
-
2026-03-11: added opt-in
--debugstartup flag; when enabled, stderr now emits sanitizedacp.messagetraces for ACP stdio and embedded-runtime request/response traffic, including session prompts, updates, and permission flows. -
2026-03-11: added ACP session browsing/resume support across built-in agents:
- introduced shared agent session abstractions for
session/list, bound-session reporting, and initialize capability parsing. - built-in providers now:
- list sessions through ACP
session/listwhen supported. - load persisted
agentOptions.sessionIdthrough ACPsession/loadbefore prompting. - report the effective session id back to HTTP turns so the server can persist it.
- list sessions through ACP
- added
GET /v1/threads/{threadId}/sessionswith cursor passthrough and gracefulsupported=falsefallback for agents without ACP transcript replay support. - turn SSE now emits
session_bound, and the server persists the thread session id without closing the active provider. - once a thread is bound to an ACP session, prompt building skips local recent-turn injection to avoid duplicating already-loaded ACP context.
- Web UI now renders a right-side session sidebar with first-page load,
Show more, active-session highlighting, andNew sessionreset. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./internal/httpapi -run 'TestThreadSessionsListEndpoint|TestTurnSessionBoundPersistsSessionIDAndSkipsContextInjection' -count=1 - pass:
go test ./...
- pass:
- introduced shared agent session abstractions for
-
2026-03-11: fixed Web UI session playback when selecting an existing ACP session from the right sidebar.
- the active chat view now treats
(threadId, sessionId)as its render scope instead of refreshing only onthreadIdchanges. loadHistory()now filters locally persisted turns by each turn'ssession_boundevent so the center chat panel replays the selected session's ngent-recorded turns instead of leaving the previous session on screen.- session changes reported mid-stream by
session_bounddefer the full chat refresh until the active turn completes, so the live streaming bubble is not destroyed.
- the active chat view now treats
-
2026-03-11: fixed Web UI history replay for legacy session threads whose
/historydata lacks per-turnsession_boundevents.- session-scoped history filtering now falls back to showing all turns when a thread has no annotated session markers at all, instead of rendering an empty chat pane despite non-empty
/history. - when a thread has exactly one annotated session, the selected session view also keeps older unannotated turns so pre-annotation history is still visible for that same session.
- session-scoped history filtering now falls back to showing all turns when a thread has no annotated session markers at all, instead of rendering an empty chat pane despite non-empty
-
2026-03-13: allowed concurrent turns across different sessions on the same thread.
- changed the runtime turn controller from thread-wide locking to
(threadId, sessionId)scoping, while keeping delete/compact/shared thread mutations guarded at whole-thread scope. - changed cached provider reuse from thread scope to session/config scope so switching sessions mid-stream no longer reuses the wrong provider instance.
- updated the Web UI to key cached messages, live stream state, and permission cards by
(threadId, sessionId), which keeps background session output from overwriting the currently visible session. - session-only
PATCH /v1/threads/{threadId}updates now succeed while another session on the same thread is active, so the right-side session switcher can move to a new session before starting the next turn.
- changed the runtime turn controller from thread-wide locking to
-
2026-03-13: fixed Web UI session switching while another session on the same thread is still streaming.
- the chat subscribe loop now forces a full chat-area rebuild when the selected
(threadId, sessionId)changes to a scope with an active stream whose bubble is not mounted in the current DOM. - returning to a background-streaming session now restores the loading/typing bubble, live partial text, and pending permission UI instead of rendering only the persisted message list.
- the chat subscribe loop now forces a full chat-area rebuild when the selected
-
2026-03-13: moved working-directory details into a chat-header Session Info popover in the Web UI.
- removed the visible working-directory line from the chat header and added an info icon that appears only when the selected session already has a persisted
sessionId. - clicking the icon now opens a
Session Infopopover showingSession IDandWorking Directory, each with a dedicated copy action; clicking outside or pressingEsccloses it. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- removed the visible working-directory line from the chat header and added an info icon that appears only when the selected session already has a persisted
-
2026-03-13: added per-session loading indicators to the Web UI session sidebar.
- the right-side
Sessionslist now shows the same spinner used by the left agent list when a specificsessionIdon the active thread is still streaming. - session items derive their loading state from scope-local
streamStates, so background activity is shown only on the matching session row. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- the right-side
-
2026-03-13: added persisted ACP slash-command support across built-in agents and the Web UI composer.
- extended shared ACP
session/updateparsing withavailable_commands_update, and wired all built-in ACP providers to forward the latest slash-command snapshot through a shared per-turn callback. - added SQLite-backed
agent_slash_commandscaching plusGET /v1/threads/{threadId}/slash-commands, so every observed slash-command update is persisted and survives server restart. - updated the Web UI composer so typing
/into an otherwise empty chat input opens a codex-style slash-command picker with keyboard navigation and command insertion for the active agent. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- extended shared ACP
-
2026-03-13: fixed Web UI freezes when an agent has no cached slash commands.
- changed the composer to fetch
GET /v1/threads/{threadId}/slash-commandslazily when the user types/, instead of preloading on thread selection. - empty slash-command snapshots now close the picker immediately and leave
/as ordinary message text, which avoids the previous empty-result refresh loop. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- changed the composer to fetch
-
2026-03-13: fixed Kimi slash-command loss in the ACP turn pipeline.
- root cause: real Kimi
kimi acpemitsavailable_commands_updateimmediately aftersession/newand beforesession/prompt, while the ngent Kimi provider had been installing itssession/updatehandler too late and silently dropped that notification. - fixed the Kimi provider to start observing
session/updatebeforesession/new/session/load, while still suppressing pre-prompt message chunks so transcript replay cannot leak into the live response stream. - added a regression test that forces
available_commands_updateto arrive duringsession/new. - executed validation:
- pass:
go test ./internal/agents/kimi -run 'TestStream(CapturesSlashCommandsEmittedBeforePrompt|WithFakeProcess|WithFakeProcessModelID)$' -count=1 - pass:
go test ./... - pass: real local ngent + Kimi test on
http://127.0.0.1:8788confirmedsession/update.available_commands_updatewas logged betweensession/newandsession/prompt - pass:
GET /v1/threads/{threadId}/slash-commandsreturned 8 persisted Kimi commands after the first turn
- pass:
- root cause: real Kimi
-
2026-03-13: forced a backend slash-command refresh on each new
/interaction in the Web UI.- root cause: once a thread had already populated the client-side slash-command cache, typing
/reused that cache and did not issue anotherGET /v1/threads/{threadId}/slash-commands, which made the real network behavior diverge from the expected "query sqlite on slash entry" flow. - changed the composer so the first bare
/in each slash interaction triggers a forced refresh for the active thread, while subsequent filtering inside the same interaction still reuses the in-memory snapshot. - the refresh guard resets when the user leaves slash mode, selects a command, sends the message, presses
Esc, or clicks outside the composer. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./... - pass: real local ngent + Kimi test on
http://127.0.0.1:8789
- pass:
- root cause: once a thread had already populated the client-side slash-command cache, typing
-
2026-03-13: fixed codex embedded slash-command loss when
session/new/session/loademitted updates before the first prompt.- root cause: the codex embedded provider subscribed to runtime updates only inside
streamOnce(), butacp-adapteremitsavailable_commands_updateimmediately aftersession/new/session/load, so the initial slash-command snapshot was dropped before ngent had any subscriber. - installed a runtime-level update monitor before
session/new/session/load, cached the latest slash-command snapshot on the provider, and replayed that cached snapshot into each turn context beforesession/promptstarts. - kept prompt-time
available_commands_updatehandling in the live stream path so later slash-command changes still propagate with the existing cancellation/error behavior. - added codex regression coverage for both the direct first-turn path and the "config options initialized the runtime before the first turn" path.
- executed validation:
- pass:
go test ./internal/agents/codex -run 'TestStream(CapturesSlashCommandsEmittedBeforePrompt|ReplaysCachedSlashCommandsAfterConfigOptionsInit)$' -count=1 - pass: real local ngent + codex test on
http://127.0.0.1:8793 - pass:
GET /v1/threads/{threadId}/slash-commandsreturned the persisted 7-command codex snapshot after the first turn - pass: sqlite
/tmp/ngent-codex-fix.dbstoredagent_id=codexwith 7 slash commands
- pass:
- root cause: the codex embedded provider subscribed to runtime updates only inside
-
2026-03-13: fixed the same pre-prompt slash-command timing bug in Qwen and OpenCode stdio providers.
- root cause: both stdio providers called
session/new/session/loadbefore registering asession/updatenotification handler, andacpstdio.Conndrops notifications when no handler is installed. - moved notification handler registration to immediately after
initialize, allowedavailable_commands_updatethrough before prompt start, and continued suppressing pre-prompt transcript chunks/plan updates so session replay cannot leak into live output. - added one fake-process regression test per provider to verify
available_commands_updateemitted duringsession/newis captured. - executed validation:
- pass:
go test ./internal/agents/qwen ./internal/agents/opencode -run 'TestStream(CapturesSlashCommandsEmittedBeforePrompt|WithFakeProcess|WithFakeProcessModelID)?$' -count=1 - pass: real local ngent + qwen/opencode test on
http://127.0.0.1:8794
- pass:
- root cause: both stdio providers called
-
2026-03-13: deduplicated ACP stdio stream notification handling across Kimi, Qwen, and OpenCode.
- extracted one shared
agents.InstallACPStdioNotificationHandler(...)helper that owns the commonsession/updateparsing path for pre-prompt slash-command snapshots plus post-prompt message/plan streaming. - removed the previous copy-pasted handler bodies from the three providers so future slash-command or plan-stream changes only need one stdio implementation update.
- executed validation:
- pass:
go test ./internal/agents/kimi ./internal/agents/qwen ./internal/agents/opencode -run 'TestStream(CapturesSlashCommandsEmittedBeforePrompt|WithFakeProcess|WithFakeProcessModelID)?$' -count=1 - pass:
go test ./...
- pass:
- extracted one shared
-
2026-03-13: applied the same pre-prompt slash-command handling and shared notification helper to Gemini.
- Gemini had the same timing bug as the other stdio-backed providers: it registered
session/updatehandling only aftersession/new/session/load, so any earlyavailable_commands_updatewould have been dropped by its customrpcConn. - generalized the shared stdio logic into
agents.NewACPNotificationHandler(...), reused it from the existing stdio helper, and wired Gemini's customrpcConnthrough the same handler builder. - added Gemini fake-process regression coverage for
available_commands_updateemitted duringsession/new. - executed validation:
- pass:
go test ./internal/agents/gemini -run 'TestStream(CapturesSlashCommandsEmittedBeforePrompt|WithFakeProcess|WithFakeProcessModelID)$' -count=1 - pass: real local ngent + Gemini test on
http://127.0.0.1:8795 - note:
GET /v1/threads/{threadId}/slash-commandsstill returned[]in that real run, which indicates Gemini CLI did not emitavailable_commands_updateon that session despite the handler now being installed early enough
- pass:
- Gemini had the same timing bug as the other stdio-backed providers: it registered
-
2026-03-13: backfilled Codex slash commands during thread config initialization so
/works before the first turn.- root cause: the Web UI opens Codex threads by loading
config-options, which initializes the embedded runtime and cachesavailable_commands_updatein provider memory, but sqlite remained empty until a later turn replayed the snapshot through the normal turn-scoped slash-command handler. - added a shared
agents.SlashCommandsProviderinterface, implemented it in the embedded Codex provider, and taught the HTTPconfig-optionspath to persist a missing slash-command snapshot from the live provider as a best-effort backfill. - added regression coverage for both the provider method and the HTTP old-thread case where config options are already stored but slash commands are still missing from sqlite.
- executed validation:
- pass:
go test ./internal/agents/codex -run 'Test(StreamCapturesSlashCommandsEmittedBeforePrompt|StreamReplaysCachedSlashCommandsAfterConfigOptionsInit|SlashCommandsAfterConfigOptionsInit)$' -count=1 - pass:
go test ./internal/httpapi -run 'Test(ThreadSlashCommandsPersistAndLoad|ThreadSlashCommandsPersistAcrossRestart|ThreadConfigOptionsBackfillsSlashCommandsWhenCatalogAlreadyStored)$' -count=1 - pass: real local ngent + codex test on
http://127.0.0.1:8796 - pass: fresh Codex thread returned the 7-command slash snapshot from
GET /v1/threads/{threadId}/slash-commandsimmediately afterGET /v1/threads/{threadId}/config-options, before any turn was sent
- pass:
- root cause: the Web UI opens Codex threads by loading
-
2026-03-13: fixed fresh-thread Qwen slash commands by probing providers on
/slash-commandscache miss.- root cause: a user could type
/before the thread-openingconfig-optionsrequest finished; for Qwen that meantGET /v1/threads/{threadId}/slash-commandsread sqlite too early and returned[]even though the provider emittedavailable_commands_updatea moment later. - implemented
SlashCommandsProvideron the Qwen stdio provider, cached slash-command snapshots inside the provider from both turn and config-session ACP notifications, and taughtGET /v1/threads/{threadId}/slash-commandsto best-effort backfill sqlite directly from the live provider when the row is missing. - added regression coverage for Qwen
ConfigOptions()followed bySlashCommands(), plus one HTTP test that verifies the slash-commands endpoint itself backfills a missing snapshot. - executed validation:
- pass:
go test ./internal/agents/qwen ./internal/httpapi -run 'Test(StreamCapturesSlashCommandsEmittedBeforePrompt|SlashCommandsAfterConfigOptionsInit|ThreadConfigOptionsBackfillsSlashCommandsWhenCatalogAlreadyStored|ThreadSlashCommandsEndpointBackfillsMissingSnapshot)$' -count=1 - pass: real local ngent + qwen test on
http://127.0.0.1:8798 - pass: fresh Qwen thread returned
/bug,/compress,/init, and/summaryfrom the very firstGET /v1/threads/{threadId}/slash-commandsbefore any turn was sent
- pass:
- root cause: a user could type
-
2026-03-13: unified provider-local ACP slash-command caching across the direct stdio agents.
- Kimi, Qwen, OpenCode, and Gemini all share the same underlying ACP behavior:
available_commands_updatecan arrive duringsession/newin both turn streaming and config-session probes, so probing slash commands only from sqlite is not enough for fresh threads. - introduced
agents.SlashCommandsCacheand wired all four direct ACP providers to feed it from bothStream()andrunConfigSession()by wrapping the request context's slash-command handler instead of duplicating provider-local cache plumbing. - each provider now implements
SlashCommandsProvider, so thread initialization andGET /v1/threads/{threadId}/slash-commandscan backfill sqlite from the live provider snapshot even before the first turn persists anything. - added config-session regression coverage for Kimi, OpenCode, and Gemini to lock in the
ConfigOptions() -> SlashCommands()path alongside the earlier Qwen test. - executed validation:
- pass:
go test ./internal/agents/kimi ./internal/agents/opencode ./internal/agents/gemini ./internal/agents/qwen -run 'Test(StreamCapturesSlashCommandsEmittedBeforePrompt|SlashCommandsAfterConfigOptionsInit|WithFakeProcess|WithFakeProcessModelID)$' -count=1 - pass:
go test ./...
- pass:
- Kimi, Qwen, OpenCode, and Gemini all share the same underlying ACP behavior:
-
2026-03-14: fixed real Kimi/Qwen ACP permission requests that carried structured
toolCallpreviews instead of flat strings.- root cause: the direct ACP provider adapters decoded
session/request_permission.toolCallasmap[string]string, but real Kimi 1.22.0 sends rich payloads withcontentarrays and diff metadata, so JSON decode failed and ngent immediately returned a fail-closed reject without emittingpermission_required. - updated Kimi and Qwen to use the shared parser, added regression coverage for real-style rich payloads, and normalized
agent_thought_chunkas the same hidden-thought update family used by the rest of ngent's ACP parser. - updated the Web UI streaming bubble to show
Thinking...immediately before the first visible delta so long Kimi reasoning phases no longer look like a dead blank bubble. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- root cause: the direct ACP provider adapters decoded
-
2026-03-14: fixed real OpenCode ACP permission requests so Web UI permission cards appear instead of silently dropping the tool call.
- root cause: after the shared ACP CLI refactor, the OpenCode adapter never installed a
HandlePermissionRequesthook, so any realsession/request_permissioncall still hit the defaultmethod not foundpath. In addition, OpenCode can encode file-like permissions viatoolCall.locations[]ortoolCall.rawInput.filepathwith generic titles such asexternal_directory. - extended the shared ACP permission parser to extract path-like previews from
locations[]andrawInput, classify directory/path requests asfile, and fall back to the resolved path when the provider title is too generic for the Web UI. - wired OpenCode into the same permission bridge as Kimi/Qwen and added regression coverage for real-style OpenCode
external_directorypayloads. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- root cause: after the shared ACP CLI refactor, the OpenCode adapter never installed a
-
2026-03-14: converged direct ACP structured-permission bridging into one shared helper in
internal/agents/acpcli.- Kimi, Qwen, and OpenCode previously carried identical provider-local
handlePermissionRequestimplementations even after sharing the same normalized permission-request parser. - moved that common bridge logic into
acpcli.StructuredPermissionRequestHandler(timeout)and reduced each provider to a one-line hook binding plus its local timeout constant. - kept Gemini unchanged because it still uses a different provider-specific permission payload/response shape rather than the shared normalized
PermissionRequestPayload. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- Kimi, Qwen, and OpenCode previously carried identical provider-local
-
2026-03-19: changed Web UI assistant rendering from one aggregated bubble into an event-ordered segment timeline.
- root cause: the Web UI already persisted
reasoning_delta,tool_call, andtool_call_update, but both live streaming and finalized history still rendered them as one concentrated reasoning/tool section plus one final assistant bubble, so the actual turn sequence was lost visually. - extended the frontend message model with ordered
segments, rebuilt finalized assistant messages from persisted turn events (message_delta,reasoning_delta,tool_call,tool_call_update), and tracked the same segment timeline during SSE streaming. - tool-call updates now keep their original timeline position by merging later
tool_call_updatepayloads into the first segment for thattoolCallId, while message/reasoning deltas append new segments only when the stream actually switches modes. - kept plan cards as a separate block, but assistant content / thought / tool activity now render in the same chronological order the agent emitted them.
- removed the IM-style left/right chat alignment for the transcript pane; user prompts now render as the top line of a single-column turn flow and agent output follows directly underneath in the same reading column.
- during an in-flight turn, only the currently growing thought segment stays expanded; once a later answer/tool/plan event arrives, the completed thought segment immediately switches to the collapsed finalized-panel state instead of waiting for full turn completion.
- completed answer segments now switch to finalized markdown rendering as soon as the stream moves on, so tables and other block markdown render immediately instead of staying as raw text until the whole turn finishes.
- styled markdown tables in answer/thought blocks with borders, header background, zebra rows, and horizontal overflow so rendered tables are visually distinct instead of looking like loosely aligned text.
- moved markdown tables onto a fit-content wrapper with independent horizontal scrolling, so the outer border now hugs the actual table content instead of stretching to the full transcript width.
- changed tool-call segments from always-open cards into collapsible panels that follow the same interaction model as thought blocks: the currently updating tool call stays open during streaming, finalized tool calls default closed, and users can manually expand closed panels on demand.
- kept permission-request cards outside that new fold/unfold treatment so approval prompts still remain independently visible and actionable while a turn is running.
- delayed hidden tool-call detail
Show allbinding until the panel is actually opened, so nested command/JSON previews still compute their collapsed height correctly after the outer tool-call panel starts closed. - moved assistant copy actions from the whole-message footer down to each finalized answer segment, so clicking copy now copies only that visible answer block instead of concatenating every answer segment in the turn.
- kept per-answer copy buttons under their own answer segments, but merged each segment's timestamp and copy control onto one small meta row so time appears first and copy follows on the same line.
- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- root cause: the Web UI already persisted
-
2026-03-20: limited runtime agent handling to providers that actually pass startup preflight in the current environment.
- root cause: ngent still used the full static provider list for
/v1/agents, create-thread allowlist validation, and startup config-catalog refresh, so hosts that only installed a subset of agent CLIs emitted noisyconfig_catalog.refresh_failedwarnings for missing binaries and exposed unusable agents in the frontend. - changed startup wiring to derive one active agent set from successful provider preflight checks and reuse that same set for frontend agent discovery, request validation, and config-catalog background refresh.
GET /v1/agentsnow omits unavailable providers instead of returningstatus:"unavailable", and startup no longer attempts config/model refresh for agents whose binaries are absent in the running environment.- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./...
- pass:
- root cause: ngent still used the full static provider list for
-
2026-03-21: deduplicated the built-in ACP provider
DiscoverModelsentrypoints.gemini,qwen,kimi, andopencodeall carried the same package-level wrapper that only calledNew(cfg)and thenclient.DiscoverModels(ctx).- added
internal/agents/acpcli.DiscoverModelsWithClientso that constructor-plus-delegate path lives in one shared helper. - updated
internal/agents/gemini/models.go,internal/agents/qwen/models.go,internal/agents/opencode/models.go, and the ACP fallback ininternal/agents/kimi/models.goto reuse that helper while preserving Kimi's local-config override behavior. - continued the same cleanup by moving the shared ACP
session/new,session/load,session/list, andsessionCWDparameter builders intointernal/agents/acpcli, then rewiredgemini,qwen,opencode, andkimito use those common helpers instead of carrying identical local functions. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
-
2026-03-22: switched model/reasoning discovery from probe sessions to real session snapshots.
- root cause: startup refresh plus
GET /v1/threads/{threadId}/config-options/GET /v1/agents/{agentId}/modelslive fallbacks were still opening probe-onlysession/newcalls, which created provider-owned empty sessions and could leak stale default config between unrelated threads. - removed startup config-catalog refresh from
cmd/ngent, and changed the HTTP config/model read paths to return stored-only sqlite data instead of probing upstream runtimes. - added a shared config snapshot callback in
internal/agents; user-initiatedsession/new/session/loadresponses from ACP stdio providers and embeddedcodex/claudenow report their latestconfigOptionsduringStream(), and session-scoped history replay can also feed the same persistence path. - the HTTP turn handler now persists those real snapshots immediately into sqlite:
threads.agent_options_jsonis updated with the session's actual currentmodelId/configOverridesagent_config_catalogsis updated under the actual current model id for later reuse
- switching a thread onto an existing session now clears stale thread-local model/reasoning selections first, so the next
session/loadbecomes the source of truth for that session's current config. - the Web UI now hides model/reasoning controls until a real config snapshot exists, reloads config options after turns finish, and clears stale thread-local config cache on session switch.
- updated HTTP tests to reflect the new lifecycle:
- fresh threads expose empty
configOptionsuntil a real turn runs - stored config rows are only used when the thread already has a concrete
modelId - agent model lists now come from stored sqlite catalogs only
- fresh threads expose empty
- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- root cause: startup refresh plus
-
2026-03-22: persisted learned config snapshots per provider session so switching back to a known session restores model controls immediately.
- root cause: after the earlier session-driven discovery change, real turns persisted config into thread state plus
agent_config_catalogs, but a later session switch intentionally cleared thread-localmodelId/configOverrides; switching back to that session then had no session-scoped snapshot to restore from. - added sqlite-backed
session_config_cache(agent_id, cwd, session_id, config_options_json, updated_at)alongside transcript caching. - real turn config snapshots now write through both:
- thread/agent-model state as before
- session-scoped config cache keyed by
agent + cwd + sessionId
- when a fresh session reports config before
session_bound, ngent now backfills the same snapshot intosession_config_cacheas soon as the session id is bound. GET /v1/threads/{threadId}/config-optionsnow restores from the current session cache when the thread points at a knownsessionIdbut has no thread-localmodelId.- user-triggered session switching now also learns config from session-scoped
GET /v1/threads/{threadId}/history?sessionId=...:- if transcript cache exists but session config cache is still missing, ngent performs one live
session/loadinstead of stopping at cached transcript data - if that
session/loadreturnsconfigOptions, ngent persists them into thread state (when the thread is currently bound to that session),agent_config_catalogs, andsession_config_cache - the Web UI refreshes config controls again after session-scoped history replay finishes, so model/reasoning buttons can appear immediately after switching sessions without needing a new turn
- if transcript cache exists but session config cache is still missing, ngent performs one live
- additionally verified the session-load-only path with a fresh Codex thread on a fresh sqlite database:
- created the thread without sending any turn
- switched directly to an existing Codex session
- confirmed
ModelandReasoningbuttons appeared immediately from that session'ssession/load
- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./... -count=1
- pass:
- root cause: after the earlier session-driven discovery change, real turns persisted config into thread state plus
-
2026-03-23: added Web UI attachment upload support that flows through ACP
resource_linkprompt content.- introduced a shared structured prompt model in the agent layer so HTTP turns can carry text plus ACP
resource_linkitems instead of being limited to one plain string. POST /v1/threads/{threadId}/turnsnow acceptsmultipart/form-data; uploaded files are persisted into the local temp directory, converted intofile://resource links, and preserved in history via a newuser_promptturn event.- ACP CLI and embedded providers now send structured
session/promptcontent arrays, while non-ACP/fallback streamers still receive a readable plain-text representation. - updated the Web UI composer to support attachment picking/removal, attachment-only sends, text+attachment sends, left-footer ordering
Attachment -> Model -> Reasoning, and user-message attachment cards reconstructed from history. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./internal/httpapi ./internal/agents/... - pass:
go test ./...
- pass:
- introduced a shared structured prompt model in the agent layer so HTTP turns can carry text plus ACP
-
2026-03-24: wired ACP
session_info_updatetitles through the turn stream into the Web UI session sidebar.- extended shared ACP
session/updateparsing withsession_info_update, ignoringtitle: nullwhile preserving non-null title changes keyed bysessionId. - added a turn-scoped session-info callback so HTTP streaming now emits
session_info_updateSSE/history events, and updated built-in ACP providers plus the shared stdio notification bridge to forward those updates. - the Web UI now applies live session-title updates to the matching sidebar session row and keeps a runtime override map so a slower follow-up
session/listrefresh does not immediately revert the new title. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./... -count=1
- pass:
- extended shared ACP
-
2026-03-22: fixed the composer model/reasoning dropdowns so they remain clickable after session transcript replay refresh updates them in place.
- root cause:
bindThreadConfigSwitches()can run more than once on the same composer DOM after a session switch; repeatedaddEventListener('click', ...)bindings caused one listener to open the menu and the next listener to close it immediately in the same click. - made config-switch binding idempotent per
.thread-model-switchnode, while still allowing later refresh passes to update labels, menu contents, and hidden/disabled state. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./... -count=1
- pass:
- root cause:
-
2026-03-22: fixed active-turn session switching when the current session had already persisted model/config state.
- root cause:
PATCH /v1/threads/{threadId}intentionally allows session-only selection changes during an active turn, but the session-only detector compared the current thread options against the target options after the target session change had already strippedmodelId/configOverrides; once the active session had learned config, that comparison incorrectly treated a pure session switch as a broader thread update and returnedthread has an active turn. isSessionOnlyAgentOptionsUpdate()now ignores thread-local config snapshot fields only when checking whether the change is a pure session/fresh-session selection change, while still rejecting unrelated active-turn thread edits such as direct model changes.- added HTTP regression coverage for switching away from an active session after its config snapshot has already been persisted.
- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./... -count=1 - pass: real browser validation with Kimi on
go run ./cmd/ngent -db-path /tmp/ngent-session-switch-active.db -port 8690 -debug: - started a new Kimi session
- triggered a long streaming response
- switched to an older session while the turn was still active
- confirmed the UI switched without the previous
thread has an active turnalert
- pass:
- root cause:
-
2026-03-22: kept active streaming sessions visible in the session sidebar after switching away.
- root cause: the Web UI only inserted the currently selected session as an ephemeral row when it was missing from
session/list; a newly created session that was still streaming but not yet present in the fetched session list disappeared from the sidebar as soon as the user switched to another session. renderSessionPanel()now also prepends any thread-local session ids that are still active instreamStates, so running sessions stay visible until the real session list catches up.- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./... -count=1
- pass:
- root cause: the Web UI only inserted the currently selected session as an ephemeral row when it was missing from
-
2026-03-22: preserved exact provider permission options end-to-end instead of collapsing every request into
Allow/Deny.- root cause: the Web UI hard-coded two buttons and
POST /v1/permissions/{permissionId}only carried a generic outcome, so multi-option ACP permission requests from providers such as Kimi could not round-tripallow once/allow always/reject once/reject alwaysexactly. - extended the shared permission model to keep provider
options[]and an exactSelectedOptionID, and taught the ACP permission bridge to prefer that exact option id when replying to option-aware providers. permission_requiredSSE payloads now include advertised permission options,POST /v1/permissions/{permissionId}now acceptsoptionIdalongside the existingoutcome, and the HTTP permission bridge infers the generic fallback outcome from the selected option kind when possible.- updated the Web UI permission card to render all advertised options dynamically, fall back to
Allow/Denyonly when no options are provided, and show the selected option label in the resolved state. - added regression coverage for exact option pass-through at the HTTP layer and for Kimi's structured permission handler preserving non-default selected option ids.
- updated API / spec / frontend docs and acceptance criteria to describe dynamic permission options.
- root cause: the Web UI hard-coded two buttons and
-
2026-03-26: moved uploaded Web UI attachments into the configurable data directory and made persisted attachment cards survive reload.
- replaced the CLI storage root flag with
--data-path(default$HOME/.ngent/), deriving sqlite asdata-path/ngent.dbinstead of accepting a standalone--db-pathfile. POST /v1/threads/{threadId}/turnsnow persists uploads underdata-path/attachments/<category>/rather than/tmp, using MIME/extension-aware categories such asimages,documents,text,audio,video,archives, andfiles.- added sqlite-backed
turn_attachments(attachment_id, turn_id, name, mime_type, size, file_path, created_at)and persist stableattachmentIdvalues intouser_promptturn events. - added
GET /attachments/{attachmentId}with client ownership checks and optional query-token auth so the Web UI can keep rendering persisted image/file cards after stream completion and history reload. - updated the Web UI history attachment reconstruction to build stable attachment URLs from
attachmentId, show persisted previews/links after reload, and keep using in-memoryblob:previews only for unsent/live local drafts. - updated README plus acceptance/decision/spec/known-issues docs to describe
--data-path, durable attachment storage, and the remaining lack of an automatic attachment janitor. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./cmd/ngent ./internal/httpapi ./internal/storage ./internal/observability - pass:
go test ./...
- pass:
- replaced the CLI storage root flag with
-
2026-03-26: reduced Web UI session-switch stalls by making thread history session-scoped.
- root cause: when the user selected one historical session, the Web UI still fetched
GET /v1/threads/{threadId}/history?includeEvents=1for the entire thread and then filtered it client-side, so a Codex thread with21turns and about19 MB/42kevents could block the UI even though the target session only needed one turn. - added optional
sessionIdfiltering toGET /v1/threads/{threadId}/history; the server now applies the same legacy-session fallback rules as the Web UI (session_boundmatching, include unannotated legacy turns only when the thread has exactly one annotated session, and continue hiding ephemeral cancelled placeholders). - updated the Web UI
loadHistory()path to requestapi.getHistory(threadId, requestedSessionID)while keeping the existing local session filter as a compatibility fallback. - verified the real problematic Codex thread on a patched local server:
- full thread history remained about
19.1 MB Response.json()time for the history payload dropped from about120-133 msto about2.5 ms
- full thread history remained about
- executed validation:
- pass:
go test ./internal/httpapi -run TestThreadHistoryFiltersBySessionID -count=1 - pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- root cause: when the user selected one historical session, the Web UI still fetched
-
2026-03-26: removed the remaining session-switch jank on old high-delta history payloads.
- residual issue: the first session-scoped fix still left old databases carrying every historical
message_delta/reasoning_deltarow in/history, so the browser could still do too much synchronous work while replaying one heavy session even though unrelated sessions were gone. - compacted consecutive same-turn history deltas server-side when serializing
/v1/threads/{threadId}/history?includeEvents=1, coveringmessage_delta,reasoning_delta, andthought_deltawithout changing persisted ordering across other event types. - kept the write-side storage merge in
AppendEvent(...)so new data no longer accumulates redundant delta rows in the first place. - changed the Web UI message-list renderer to yield across animation frames for larger histories instead of rebuilding the whole chat synchronously in one pass.
- real repro on the provided Codex session after both changes:
- browser
Response.json()for that history payload measured about1.3 ms hello -> first sessionreplay no longer showed a>50 msmain-thread gap; measured max RAF gap was about9.4 mson the patched local server
- browser
- residual issue: the first session-scoped fix still left old databases carrying every historical
-
2026-03-26: let the Web UI browse other sessions while one turn is still streaming.
- root cause: the session sidebar treated "currently viewed session" and
thread.agentOptions.sessionIdas the same thing, so clicking another session during an active turn always triedPATCH /v1/threads/{threadId}and surfaced409 thread has an active turn. - introduced a local per-thread session-selection override in the Web UI; when a thread already has an active stream, session switching now only changes the visible chat scope and history target instead of immediately patching backend thread state.
- deferred backend
sessionIdsync until the active turn finishes, and also guard the send path by syncing any pending local selection before starting the next turn so new prompts still land in the session currently shown in the UI. - kept the existing fresh-session behavior by assigning stable local
@fresh:<nonce>scopes, and cleaned those local overrides up when the thread is deleted. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- root cause: the session sidebar treated "currently viewed session" and
-
2026-04-06: unified Web UI session replay behind the session-scoped
/historyendpoint.- root cause: opening one selected session in the embedded Web UI still required two backend requests and two separate client-side code paths:
/history?includeEvents=1&sessionId=...for ngent-owned turns plus a second dedicated provider transcript replay request. - extracted the backend session-transcript loader into one shared helper, and taught
GET /v1/threads/{threadId}/history?sessionId=...to optionally includesessionTranscript { supported, messages }alongside the existing filteredturns. - changed the Web UI
loadHistory()path to issue onlyapi.getHistory(threadId, requestedSessionID)and merge any embeddedsessionTranscriptreplay onto the persisted turn history, preserving the existing cache hydration and config-refresh behavior. - if provider replay fails,
/historynow still returns the persisted turns and omitssessionTranscript, matching the Web UI's previous local-history fallback behavior instead of failing the whole session switch. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./internal/httpapi -run 'Test(ThreadHistoryEndpointIncludesSessionTranscriptForSelectedSession|ThreadHistoryEndpointPersistsConfigOptionsForSelectedSession|ThreadHistoryEndpointUsesCachedSessionTranscriptAcrossRestart|ThreadHistoryEndpointReloadsLiveWhenTranscriptCachedButConfigMissing|ThreadHistoryEndpointKeepsTurnsWhenSessionTranscriptLoadFails|ThreadHistoryFiltersBySessionID)$' -count=1 - pass:
go test ./...
- pass:
- root cause: opening one selected session in the embedded Web UI still required two backend requests and two separate client-side code paths:
-
2026-04-06: removed the standalone provider transcript replay endpoint after migrating all callers to session-scoped
/history.- deleted the dedicated provider transcript replay router entry and its handler; session transcript replay is now reachable only through
GET /v1/threads/{threadId}/history?sessionId=.... - removed the unused dedicated Web UI transcript replay client method and the old endpoint-specific HTTP tests.
- rewrote the remaining transcript/cache/config regression coverage to target
/history?sessionId=...directly, including unsupported-provider and cache-miss-with-config-refresh scenarios. - updated API/spec/acceptance/known-issues/decision docs so the current contract no longer documents a standalone transcript replay endpoint.
- executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./internal/httpapi -run 'Test(ThreadHistoryEndpointIncludesSessionTranscriptForSelectedSession|ThreadHistoryEndpointPersistsConfigOptionsForSelectedSession|ThreadHistoryEndpointUsesCachedSessionTranscriptAcrossRestart|ThreadHistoryEndpointReloadsLiveWhenTranscriptCachedButConfigMissing|ThreadHistoryEndpointSessionTranscriptUnsupported|ThreadHistoryEndpointKeepsTurnsWhenSessionTranscriptLoadFails|ThreadHistoryFiltersBySessionID)$' -count=1 - pass:
go test ./...
- pass:
- deleted the dedicated provider transcript replay router entry and its handler; session transcript replay is now reachable only through
-
2026-04-07: integrated Pi Agent as a new embedded provider.
- added
internal/agents/pion top ofacp-adapter/pkg/piacp, covering embedded runtime startup, prompt streaming, config/model discovery, session list/load replay, slash-command caching, session-usage propagation, and fail-closed permission replies. - wired Pi into
cmd/ngent/main.gostartup preflight, supported-agent registration, lazy per-thread provider resolution, and model discovery so/v1/agentscan now surface{"id":"pi","name":"Pi Agent"}when thepibinary is available. - updated the embedded Web UI to render the provided
public/pi.svgin the thread rail andNew Agentmodal, adding a dark icon backing only in light theme because the source asset is pure white. - updated README / SPEC / ACCEPTANCE / DECISIONS / KNOWN_ISSUES memory docs to record the embedded Pi design and its current upstream limitations.
- executed validation:
- pass:
go test ./internal/agents/pi -count=1 - pass:
cd internal/webui/web && npm run build - pass:
env GOCACHE=/tmp/ngent-gocache GOFLAGS=-p=1 /usr/local/go/bin/go test ./...
- pass:
- added
-
2026-04-13: added a per-turn Codex full-access override and a Codex-only Web UI switch.
- extended
/v1/threads/{threadId}/turnsrequest decoding with optionalfullAccess, including multipart turn uploads, without changing persisted threadagentOptions. - mapped
fullAccess=trueonly for Codex turns to one-shot runtime overridesapprovalPolicy=neverandsandbox=danger-full-access, carried through turn context into the embedded Codex provider'ssession/promptrequest. - replaced the composer footer hint text with a Codex-only
Full accessswitch in the Web UI; the switch state is local to the current unsent chat scope and is cleared immediately after send so the override applies only to that turn. - added backend tests covering Codex-only runtime override injection plus embedded Codex runtime propagation to fake app-server
turn/start. - executed validation:
- pass:
cd internal/webui/web && npm run build - pass:
go test ./...
- pass:
- extended