Strict OpenAI clients (e.g. opencode / the Vercel AI SDK) validate every
streaming `data:` frame as a chat.completion.chunk. The id / source /
tool_call / tool_calls_pending events were emitted as bare `{"docsgpt": ...}`
objects with no `choices`, which those clients reject ("expected array,
received undefined" on `choices`).
Wrap the extension in an otherwise-empty chunk
(choices:[{index:0,delta:{},finish_reason:null}]) with a top-level `docsgpt`
field that OpenAI clients ignore, and skip the placeholder "None"
conversation_id frame emitted when the call is not persisted.
Make the OpenAI-compatible Chat Completions endpoint honor per-request
Structured Outputs and keep it OpenAI-compatible.
- translate_request now forwards the request's `response_format` (json_schema)
or a `response_schema` convenience field to the agent as its json_schema,
overriding the agent-configured schema for that request.
- Honor `response_format.json_schema.strict` (default true); strict:false
passes the schema through without forcing additionalProperties:false /
all-required (OpenAI's lenient mode).
- Support `response_format {"type":"json_object"}` via the provider's native
JSON mode.
- Keep `content` clean: some models echo their reasoning into content as
stringified `{'type': 'thought', ...}` reprs when response_format is set.
Strip those from content and reroute them to `reasoning_content` (OpenAI
never puts reasoning in content), for both streaming and non-streaming.
Flow: translate_request -> StreamProcessor._configure_agent ->
Agent.json_schema / json_schema_strict / json_object -> _llm_gen ->
prepare_structured_output_format(schema, strict).
Live /v1/responses rejects input_text for the assistant role
(input[N].content[0] -> "Supported values are: 'output_text' and
'refusal'"). Revert the previous review-driven change: encode assistant
turns as output_text and user/system turns as input_text. The SDK type
union suggested input_* only, but the API enforces output_text per-role —
confirmed against the live endpoint.
- Replay prior-turn assistant text as input_text, not output_text: a
Responses easy-input message only accepts input_* content parts, so the
old shape would 400 on the second turn of every conversation (default
store=false resends prior turns inline).
- Always request include=["reasoning.encrypted_content"] so in-turn
reasoning carryover works whether or not the response is also stored
server-side (OPENAI_RESPONSES_STORE).
- Add detail="auto" to input_image parts.
- Remove the now-dead azure_openai option from setup.sh / setup.ps1 and
the docs, completing the AzureOpenAILLM removal.
- Tests: correct the input_text / image-detail / include assertions and
add parallel tool-call and stream-error coverage.
Reconcile the reasoning-passthrough change (#2520) with the Responses API
work: dedupe the shared reasoning_effort capability, keep upstream's
upstream_model_id YAML plumbing and the _apply_reasoning_effort chat path,
and run the Responses branch ahead of it so responses-flavor models skip
the chat-only top-level reasoning_effort injection.
Route opted-in models to /v1/responses while keeping Chat Completions as
the default. The flavor is selected per-model in the catalog YAML and
threaded through ModelCapabilities (with a new reasoning_effort field).
- Translate chat-shaped messages/tools/structured-output into the
Responses input / text.format / flattened-tool shapes at the provider
boundary; stored history and intra-turn tool round-trips need no
migration.
- Normalize the typed SSE event stream back into the existing handler
contract (content / thought / tool-call chunks) so the streaming
accumulator and OpenAI handler stay unchanged.
- Carry encrypted reasoning items across the in-turn tool loop
(provider-local, keyed by call_id) for stateless reasoning continuity.
- OPENAI_RESPONSES_STORE opt-in enables server-side state and
previous_response_id chaining across turns, persisted in message
metadata.
- Set gpt-5.5 to api_flavor: responses, reasoning_effort: medium.
- Remove AzureOpenAILLM and the azure_openai provider/catalog.
* fix: route remote-device tool through Redis so scheduled runs reach the device
The remote-device tool worked interactively but timed out on every scheduled
run. DeviceBroker was an in-process, in-memory singleton, but scheduled runs
execute in the Celery worker — a different process from the gunicorn web tier
that holds the device's SSE session — so a worker-side dispatch never reached
the device and the tool always hit its deadline.
Make the broker Redis-backed so every hop crosses the process boundary:
- queued commands -> Redis list dev:cmd:{device_id}
- output chunks -> Redis stream dev:out:{invocation_id}
- invocation metadata -> Redis hash dev:inv:{invocation_id}
- SSE upgrade tickets -> Redis key dev🎫{device_id}
Per-connection SSE session state stays in the web process. Reuses the existing
get_redis_instance()/CACHE_REDIS_URL; no new infrastructure. Also makes the web
tier safe to scale past one worker.
Concurrency hardening (from adversarial review + real-Redis e2e):
- XADD the output/control chunk before flipping completed=1, and have
drain_output do a final non-blocking flush after observing completion, so a
reader can't see completion and stop before the control chunk lands (this had
reintroduced the false "device did not respond (timed out)" under a race).
- _collect_result builds the result from drained chunks, checks the deadline
only after capturing a chunk, and falls back to the authoritative snapshot
(before cleanup) when no control chunk was observed.
- Audit outcome is written from locally-known fields so it survives the worker
racing to delete the invocation; a denied command now records a terminal
"denied" outcome instead of staying "dispatched".
- cmd-queue TTL raised to 900s (>= max drain deadline); dispatch-failure and
reaped-invocation cleanup; UTF-8 byte counts.
Tests: new tests/devices/{conftest (FakeRedis double), test_broker_cross_process,
test_broker_race, test_submit_output_audit}; drain/cleanup/ticket tests rewritten
for the Redis contract. The race tests fail against the pre-fix code. ruff clean;
device + tool-executor suites green.
* fix: log instead of silently passing on failed-dispatch cleanup
Addresses the code-quality lint on the best-effort hash delete in
dispatch_invocation's failure path: replace the bare `except: pass` with a
logger.debug carrying the invocation_id. No behavior change — cleanup stays
best-effort and still returns a failed Invocation.
* feat: make mobile popovers as sheet
* feat: sheets for other big modals mobile
* (fix):multiselectpopover, prevent placeholder re-render
* fix: google drive thing
* chore: push new notification text
* revert: remove vite dev server overrides
Drops the host/allowedHosts block added for local Tailscale testing —
it shouldn't have been committed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: ManishMadan2882 <manishmadan321@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(frontend): add Modal primitive on Radix Dialog (P1.1)
Adds frontend/src/components/ui/modal.tsx — a thin wrapper around
the existing Dialog/DialogContent primitives that preserves the
legacy WrapperModal visual contract (bg-card, rounded-2xl, p-8,
blurred backdrop) while inheriting Radix's focus-trap, body-scroll
lock, and Esc handling. Always renders a DialogTitle (wrapped in
VisuallyHidden when hideTitle is set) so modals stop logging a11y
warnings on adoption. No consumers migrated yet.
* refactor(frontend): migrate ConfirmationModal to Modal primitive (P1.2)
Swaps WrapperModal for the new Radix-backed Modal so consumers
inherit focus trap, body scroll lock, and Esc-to-close. Footer
DOM order flipped from [Submit, Cancel] (flex-row-reverse) to
[Cancel, Submit] so tab order now matches visual order; visual
layout unchanged. Message renders as DialogTitle for proper
screen-reader announcement.
* refactor(frontend): inline DeleteConvModal into Navigation (P1.3)
DeleteConvModal was a thin Redux wrapper around ConfirmationModal
with a single consumer and a dead useOutsideAlerter on an unbound
ref. Inlined the direct ConfirmationModal call at the call site
and deleted the wrapper file.
* refactor(frontend): migrate 5 small modals to Modal primitive (P1.4)
Migrates FolderManagementModal, AddActionModal, JWTModal,
ShareConversationModal and SearchConversationsModal off WrapperModal
onto the Radix-backed Modal so they gain focus trap, scroll lock,
and consistent DialogTitle a11y. Footer DOM order normalised so
tab order matches visual order. JWTModal stays intentionally
uncloseable via isPerformingTask; P1.7 revisits.
Also wraps DialogDescription in VisuallyHidden when no description
is provided, silencing the Radix "Missing Description" console
warning that consumers without a subtitle were triggering.
* refactor(frontend): migrate 5 medium modals to Modal primitive (P1.5a)
Migrates AddToolModal, AgentDetailsModal, ConfigToolModal,
ImportSpecModal and MoveToFolderModal off WrapperModal. They now
inherit focus trap, scroll lock, Esc-to-close and consistent
DialogTitle a11y. Footers reordered to natural DOM order so tab
order matches visual order. AddToolModal's dead useOutsideAlerter
on an unbound ref removed.
* refactor(frontend): migrate 5 large modals to Modal primitive (P1.5b)
Migrates PromptsModal, CustomModelModal, MCPServerModal,
ScheduleFormModal and Upload off WrapperModal. These were the last
remaining consumers of the legacy modal wrapper outside of the
shared WrapperModalPropsType (kept temporarily for P1.9 cleanup).
CustomModelModal continues to set isPerformingTask while saving;
P1.7 revisits Esc behavior during locked submits.
* refactor(frontend): migrate AgentTypeModal to Modal primitive (P1.6)
AgentTypeModal was the last hand-rolled fixed-inset overlay in the
codebase. Swapping to Modal gives it focus trap, Esc-to-close,
body scroll lock and consistent DialogTitle a11y — none of which
the legacy overlay implemented.
* fix(frontend): let users dismiss CustomModel and Schedule modals during save (P1.7)
Drops isPerformingTask on CustomModelModal and ScheduleFormModal so
Esc / click-outside / the X button all close the modal while a
save is in flight. The Save button itself stays disabled so users
can't double-submit. SchedulesView.closeModal was also gating on
submitting; that lock is removed too. The in-flight network call
continues in the background.
JWTModal remains intentionally uncloseable — it gates app access
on auth, not a transient save state.
* style(frontend): normalise modal CTA radius to rounded-3xl (P1.8)
ConfigToolModal, JWTModal, ShareConversationModal and
ScheduleFormModal previously used rounded-full for their primary
CTAs while the rest of the modal suite uses rounded-3xl. Switches
the four outliers and pads font-medium where missing. Footer DOM
order across all migrated modals was already corrected to natural
order during P1.2 / P1.4 / P1.5, so no further reorderings here.
* chore(frontend): delete legacy WrapperModal (P1.9)
All 14 consumers were migrated to the Radix-backed Modal primitive
across P1.2-P1.6. Drops the legacy WrapperModal.tsx, the orphan
WrapperModalPropsType type re-export, and updates the stale z-index
comment in TimezoneCombobox to reference Modal's z-50.
* feat(frontend): add floating-label variant to ui/input (P2.1)
Extends the shadcn Input primitive with an optional label prop and
leftIcon/required/labelBgClassName slots so consumers can swap the
legacy components/Input.tsx without losing the peer-based floating
label UX. Existing bare-input usage is unchanged.
* refactor(frontend): migrate 9 callers off components/Input.tsx (P2.2)
Switches ToolConfig, PromptsModal, AddActionModal, JWTModal,
ToolsPopup, SourcesPopup, MultiSelectPopup, FilePicker and Upload
to the ui/input floating-label variant. Drops legacy knobs that
the audit flagged as cruft (colorVariant, borderVariant, textSize,
edgeRoundness); inputs converge on rounded-md and 1px borders.
The biggest visible delta is PromptsModal's prompt-name field —
pill -> rounded-md rectangle, intentional.
* chore(frontend): delete legacy components/Input.tsx (P2.3)
All 9 consumers were migrated in P2.2. Removes the legacy
component and the unused InputProps type from components/types.
* refactor(frontend): migrate 7 raw text inputs to ui/input (P2.4)
Swaps the page-level search inputs on Sources, Tools, CustomModels
and AgentsList, plus the inline new-folder input on AgentsList,
the FolderManagementModal field, and SearchConversationsModal,
onto the ui/input primitive. Page searches now use the floating
label / leftIcon slot. Complex per-method or workflow-editor inputs
deferred. Also clears the trailing-newline lint error in
modals/types after the WrapperModalPropsType deletion.
* feat(frontend): add destructive-outline button variant (P2.5)
Adds the red-outlined danger button pattern (border-destructive
text-destructive on transparent ground, filling to bg-destructive
on hover) so callers like "Delete All" CTAs can adopt the
primitive instead of hand-rolling the class string.
* refactor(frontend): migrate settings/ buttons to ui/button (P2.6)
Replaces 10 raw <button> elements across General, Sources, Tools,
CustomModels, Prompts and ToolConfig with the ui/button primitive.
General's Delete-All CTA adopts the new destructive-outline variant.
Tiny method-row buttons inside ToolConfig with bespoke 50px inline
widths are deferred to a later targeted pass.
* refactor(frontend): migrate agents/ buttons to ui/button (P2.7)
31 raw <button> elements across AgentsList, NewAgent, the schedules
view + form + run drawer, SchedulerToolCallCard, RunLog,
SharedAgentCard, and WorkflowPreview converted to the ui/button
primitive. NewAgent's Danger-Zone Delete CTA adopts the new
destructive-outline variant. AgentTypeModal's two type cards stay
raw — the lucide icon sizes would conflict with Button's SVG
selector — but pick up focus-visible ring classes for a11y.
WorkflowBuilder (18 buttons) and PromptTextArea deferred.
* refactor(frontend): migrate modal + small-component buttons to ui/button (P2.8)
52 raw <button> elements across 11 modals (ConfirmationModal,
FolderManagementModal, AddActionModal, ConfigToolModal,
ImportSpecModal, ShareConversationModal, AgentDetailsModal,
MCPServerModal, CustomModelModal, JWTModal, SearchConversationsModal)
and 10 small components (ActionButtons, CopyButton,
TextToSpeechButton, Help, DocumentPagination, SettingsBar,
ConnectorAuth, UploadToast, Accordion, Notification) converted to
the ui/button primitive. ConfirmationModal's danger variant now
uses the canonical destructive variant rather than a class string.
MoveToFolderModal (purple-hex inputs) plus all chat/file-tree
internals deferred.
* refactor(frontend): collapse 3 popups into MultiSelectPopover (P2.9+P2.10)
Adds frontend/src/components/MultiSelectPopover.tsx — a single Radix
Popover + cmdk Command-backed primitive supporting groups, search,
icons, loading, footer slot, and (selected-first) ordering. Migrates
MessageInput (Sources + Tools), NewAgent (Sources + Tools + Models)
onto it, then deletes MultiSelectPopup, SourcesPopup and ToolsPopup
(~803 LoC removed). The isChatToolVisible helper moves to
utils/toolUtils.ts with its tests preserved. Visual change: drops
the bespoke Material 3-layer shadow in favor of shadow-md.
* refactor(frontend): collapse 3 custom dropdowns onto shadcn primitives (P2.11)
Deletes Dropdown.tsx, DropdownMenu.tsx and DropdownModel.tsx
(~513 LoC) and migrates the 10 consumers (Hero, Sources, Prompts,
PromptsModal, Analytics, ToolConfig, General, ShareConversationModal,
Upload, NewAgent). Simple cases land on ui/select; action menus on
ui/dropdown-menu; the searchable prompts picker uses ui/popover +
ui/command. Inline edit/delete on prompts rows preserved via a small
combobox; variable menus in PromptsModal use ui/dropdown-menu.
* refactor(frontend): migrate Sidebar + ArtifactSidebar to ui/sheet (P2.12)
Deletes Sidebar.tsx and rewrites ConversationBubble to render the
citations panel directly via ui/sheet. ArtifactSidebar keeps its
public API but its overlay variant now uses ui/sheet instead of a
hand-rolled fixed-position overlay + manual click-outside, so it
inherits Radix's focus trap, Esc handling, and scroll lock.
* refactor(frontend): migrate ContextMenu to ui/dropdown-menu (P2.13)
Deletes ContextMenu.tsx (177 LoC) and migrates its 8 consumers
(ConversationTile, FolderCard, AgentCard, settings/CustomModels,
settings/Tools, settings/Sources, FileTree, ConnectorTree) to the
Radix-backed ui/dropdown-menu. Destructive items use the canonical
variant; each menu trigger uses stopPropagation so clicking the
3-dots no longer fires the parent row's onClick.
* refactor(frontend): retire the legacy color palette (P0.1+P0.2)
Migrates the last 13 consumers of legacy named-color tokens to
the semantic system (bg-primary, bg-muted, text-foreground,
border-border, etc.) and then deletes the ~60-token legacy
@theme block in index.css, including the invalid 5-digit
--color-just-black: #00000 entry. Three intentional visual
shifts: ToggleSwitch on-state moves from green to bg-primary,
PageNotFound CTA from the misnamed bg-blue-1000 to bg-primary,
and the user-message bubble gradient from medium-purple/slate-blue
to violet-500/violet-600.
* refactor(frontend): replace hardcoded purple hexes with semantic tokens (P0.3)
Removes #7D54D1 / #976af3 / #6A4DF4 / #563DD1 / #8C67D7 / #6F3FD1
/ #D9534F literals from ~15 tsx files, mapping each to bg-primary /
text-primary / border-primary / bg-destructive / hover:bg-primary/90.
The Spinner primitive drops its color prop and inherits currentColor
so it picks up the parent's Tailwind text class. Analytics chart
colors now read --primary at runtime via getComputedStyle on
document.body (where the .dark class lives) so dark mode renders
#976af3 instead of the light #7d54d1.
PromptsModal's bespoke #D9534F danger maps to bg-destructive.
MoveToFolderModal's purple-branded inputs and several toast banners
were also caught in the sweep.
* feat(frontend): add --font-mono token, drop inline IBMPlexMono styles (P0.4)
Defines a --font-mono token in @theme and points it at the existing
IBMPlexMono-Medium @font-face fallback chain. The 6 inline
style={{ fontFamily: 'IBMPlexMono-Medium' }} occurrences in
ConversationBubble's tool-call panels are replaced with the
font-mono Tailwind utility; .logs-table now references the token
too. Tokenises the mono surface so future consumers don't need
inline styles.
* feat(frontend): token-ize modal + toast shadows (P0.5)
Adds --shadow-modal and --shadow-toast tokens in @theme. The two
arbitrary shadow strings (the WrapperModal-era 4-40 modal shadow
and the 24-48 toast shadow) collapse to `shadow-modal` and
`shadow-toast` utilities across Modal, WorkflowBuilder's settings
panel, UploadToast and ToolApprovalToast. The one-off radial hover
halo in Upload.tsx stays inline (not part of the modal/toast set).
* refactor(frontend): collapse 49 arbitrary radii onto canonical scale (P0.6)
Maps every rounded-[Npx] (and its rounded-t-*/rounded-b-* variants)
to a Tailwind built-in: rounded-md/lg/xl/2xl/3xl/full. Includes
NewAgent's 11 panel sections (rounded-2xl), AgentCard/FolderCard
(rounded-2xl), Hero demo CTAs and message-input pills (rounded-full),
the answer/thought/user-message bubbles (rounded-2xl/3xl), code
blocks (rounded-md/xl), and the FileTree/ConnectorTree search
chrome. Closes out the audit's "six different radius scales"
finding.
* refactor(frontend): standardize type scale + fix font typos (P0.7)
Collapses ~80 arbitrary text-[Npx] utilities onto the canonical
Tailwind scale (text-xs/sm/lg/xl/2xl/3xl/4xl), fixes the
font-semi-bold typo in SharedConversation (rendered as font-normal
before), and removes the undefined font-inter class from 10 sites —
body already defaults to Inter via index.css. The largest visual
shift is text-[10px] -> text-xs (2px growth on tiny chip labels);
mid-range deltas are <=1px.
* style(frontend): soften popover/select/dropdown/command radii to rounded-xl
The pill-shaped (rounded-3xl, effectively fully rounded at h-9/h-10)
select triggers in Settings/Analytics/ToolConfig/NewAgent looked
mismatched against the shadcn-default rounded-md (6px) popover
content. Bumps the four ui content panels to rounded-xl (12px) so
the trigger -> popover transition reads as a single step on the
radius scale rather than an 18px jump.
* style(frontend): align Select popover to trigger width, merge Hero model picker
ui/select: switches default position from item-aligned to popper so
Radix exposes --radix-select-trigger-width on the popper container,
adds w-(--radix-select-trigger-width) so the popover width tracks
the trigger exactly, and drops the min-w-32 floor that was forcing
narrow triggers like the Analytics filter (w-[125px]) to render
3px wider than their triggers. Side-effect: Analytics dropdowns no
longer overlap the settings tab bar (popper anchors below).
Hero: pairs the trigger's data-[state=open]:rounded-b-none with
rounded-t-none + rounded-b-4xl + data-[side=bottom]:translate-y-0
on the popover so the two shapes merge into one continuous pill
when open.
* refactor(frontend): fold two skeletons into SkeletonLoader (P3.1)
Adds connectedState + filesSection variants to SkeletonLoader and
sweeps the existing variants from bg-gray-200/700/bg-gray-300/600
onto the semantic bg-muted / bg-muted-foreground/20 pair. Deletes
the two standalone files and migrates GoogleDrivePicker to the
variant API. One skeleton tone across the app.
* chore(frontend): drop legacy spinner SVGs + last raw animate-spin (P3.2)
Migrates Navigation, ShareConversationModal, TextToSpeechButton
and MultiSelectPopover off three SVG spinner assets onto the
currentColor Spinner component, then deletes assets/spinner.svg,
spinner-dark.svg and Loading.svg. Also replaces the last raw
animate-spin border-b-2 loader with Spinner.
* refactor(frontend): inline SVG components -> assets/*.svg?react (P3.3)
Deletes SendArrowIcon.tsx and RetryIcon.tsx. The send-arrow path
moves into assets/send.svg (overwriting the unused paper-plane
icon), the retry path into a new assets/retry.svg; both use
fill="currentColor" so consumers theme via Tailwind text classes.
MessageInput and ConversationMessages now import the SVGs via
?react. ConversationMessages drops a now-unused useDarkTheme hook
that was switching the retry icon's fill/stroke manually.
documentation-dark / no-files-dark / science-spark-dark variants
left in place — they're hand-tuned dark palettes, not pure inverts,
so collapsing them would regress visually.
* refactor(frontend): unify close icons on lucide X (P3.4)
Migrates the last three assets/exit.svg consumers (MessageInput
attachment pills, ArtifactSidebar split + sheet variants,
ConversationTile rename cancel) plus two inline-SVG X paths
(UploadToast dismiss, ToolApprovalToast dismiss) to lucide X.
Deletes assets/exit.svg. lucide X uses currentColor so the
filter dark:invert plumbing also goes away. ConversationTile's
bare clickable img becomes a proper button.
* refactor(frontend): merge Avatar + AgentImage into ui/avatar (P3.5)
Builds a single components/ui/avatar.tsx that subsumes both legacy
files: takes src/alt/fallbackSrc/className/imgClassName for the
smart-img case, and falls back to children passthrough for the
custom-node case (e.g. the user-question avatar in
ConversationBubble). All 6 consumers (Navigation, AgentCard,
SharedAgentCard, SharedAgent, ConversationBubble x5, ResearchProgress)
migrated, then Avatar.tsx and AgentImage.tsx deleted. The onError
robot-fallback behavior is preserved.
* refactor(frontend): promote ToggleSwitch/SettingsBar/Accordion/Table -> ui/* (P3.6)
Promotes four legacy components to shadcn-style primitives:
- ui/switch (Radix Switch) replaces ToggleSwitch; 7 callsites
migrate, including 3 inline hand-rolled toggles in NewAgent.
- ui/tabs (Radix Tabs) replaces SettingsBar; the settings page
keeps its mobile horizontal-scroll arrow wrapper.
- ui/accordion (Radix Accordion) replaces Accordion; consumed by
ConversationBubble tool-call panels.
- ui/table (plain <table>) replaces Table; tokenises bg-gray-100
-> bg-muted and borders -> border-border; 3 tree-browser
consumers swap imports.
Adds @radix-ui/react-switch / -tabs / -accordion as explicit deps
(previously transitive via the radix-ui meta-package).
* refactor(frontend): dedupe FileTree + ConnectorTree (P3.7)
Extracts a shared TreeBrowser into components/tree/ that handles
directory state, breadcrumb header, search dropdown, table render,
Chunks pickup, row-menu API and an SSE-terminal-wait hook. The
two wrappers shrink to thin shells holding only what differs: file
upload + delete flow on FileTree, sync flow on ConnectorTree. The
Sources.tsx consumer API is unchanged. 1895 -> 1391 LoC (~27%
reduction).
* style(frontend): unify settings card padding to p-5 (P4.1)
Sources tiles were p-3, Tools tiles p-6, CustomModels p-5. Picks
p-5 as the median and applies to Sources and Tools so the three
tile families breathe the same way.
* style(frontend): promote Settings Prompts Add to filled CTA (P4.2)
The Add prompt button was visually outlined-as-primary (variant=outline
+ border-primary/text-primary/hover:bg-primary overrides). Adding a
prompt is the page's main action — drop the overrides and use the
canonical default (filled bg-primary) variant so the CTA reads as
primary at a glance.
* style(frontend): add dark variants to WorkflowBuilder palette tiles (P4.3)
The five node-picker tiles (AI Agent purple, End green, Note yellow,
Set State blue, If/Else orange) used bg-{color}-100 with no dark:
companion, leaving them blindingly bright on the dark canvas. Pairs
each with bg-{color}-900/40 + dark:text-{color}-300 so they sit on
the dark workflow background.
* refactor(frontend): dedupe METHOD_COLORS + extract NARROW_CELL (P4.4)
ToolConfig and ImportSpecModal both declared an identical
METHOD_COLORS map for HTTP-verb pills. Moves the map + a
getMethodColorClass helper into utils/httpMethodColors.ts and
points both consumers at it.
ToolConfig also had 9 identical style={{ width: '50px', ... }}
inline blocks on table cells. Replaces them with a NARROW_CELL
Tailwind constant. The `!` suffix is required because the
codebase's @utility table-default rule otherwise out-specifies
the utility classes — inline styles were load-bearing for that
reason.
* style(frontend): drop MoveToFolderModal inline fonts + tokenize hover (P4.5)
Removes the inline fontFamily 'Inter' / 'Segoe UI' overrides — body
already inherits Inter via index.css — and collapses the inline
font-size/line-height/letter-spacing on the title into Tailwind
utilities (text-2xl leading-7 tracking-[0.15px]). Replaces the
breadcrumb hardcoded text-[#59636E] with text-muted-foreground and
swaps the chevron's hover:bg-[#FFFFFF2B] for hover:bg-accent.
* refactor(frontend): migrate ToolConfig method-row buttons to ui/button (DEF.1)
13 raw <button> elements left over from the P2.6 settings sweep
(method-row trash inside NARROW_CELL, rename confirm/cancel,
Add/Cancel/Add-new footers in the Properties + Headers tables)
now go through the ui/button primitive. Action-row trash retains
its stopPropagation so clicking the icon doesn't collapse the row.
* refactor(frontend): migrate conversation/* buttons to ui/button (DEF.2)
21 raw <button> elements across ConversationBubble (15),
ConversationMessages (2), ConversationTile (2), SharedConversation
(1) and ResearchProgress (1) now go through ui/button. Includes
question collapse/edit, edit submit/cancel, like/dislike feedback,
copy / regen / retry, citation pills, tool-call approve/deny,
sources/thought/tool-calls accordions, scroll-to-bottom and the
shared-conversation CTA. All stopPropagation handlers preserved.
* refactor(frontend): migrate MessageInput buttons to ui/button (DEF.3)
Migrates the 7 raw <button> elements in the 1768-LoC chat input
(attachment chip remove, Sources/Tools popover triggers + footer
upload CTA, voice/mic toggle, stop-streaming, send/submit) onto
ui/button. The 3 remaining <input> elements are file/dropzone
inputs (react-dropzone hidden + attach + voice picker) and are
intentionally left as-is.
* refactor(frontend): migrate WorkflowBuilder buttons + inputs (DEF.4)
17 of 18 raw <button> elements migrated to ui/button (back-to-all,
pencil-edit, settings Done, toolbar Details/Access-Details/Delete/
Preview/save, error-alert close, node-config close, state-op add/
delete, condition case add/delete, simple/advanced segmented control,
delete-node). The 1 remaining raw button is the custom toggle
switch for allow_system_prompt_override which uses a non-button
animated UI.
7 of 8 raw <input> migrated to ui/input (workflow name, node title,
output variable, target variable, case name, simple-mode var/value).
The 1 remaining raw input is the stream_to_user checkbox.
Palette tiles, drag-and-drop, React Flow node interactions all
preserved.
* refactor(frontend): migrate utility-component buttons to ui/button (DEF.5)
25 raw <button> elements across 7 small utility components migrated
to ui/button: Chunks (8), MermaidRenderer (5, low-risk path keeps
the custom download wrapper), GoogleDrivePicker (3),
FilePicker (3), tree/TreeBrowser (2), workflow PromptTextArea (2),
ArtifactSidebar (2). All onClick/disabled/aria/stopPropagation
preserved; MermaidRenderer's brand colors and Drive-picker's
#A076F6 brand override kept inline.
* refactor(frontend): migrate page-level buttons to ui/button (DEF.6)
27 raw <button> elements across Navigation (6), preferences
PromptsModal (5), Upload (3), settings Prompts (3), MoveToFolderModal
(8) and ToolApprovalToast (2) migrated to ui/button. Includes the
sidebar collapse / new-chat / mobile-hamburger / agent-pin row in
Navigation, both Add and Edit prompt modal flows, the breadcrumb +
folder-row + Cancel/Move CTAs in MoveToFolderModal, Upload back
chevron / advanced toggle / Train submit, the toast Dismiss + Review
pair, and the Settings Prompts popover trigger + per-row pencil/trash
buttons.
* refactor(frontend): final raw <input> sweep onto ui/input (DEF.7)
25 text/search/number inputs migrated to ui/input across:
ToolConfig (14 method-row + search), ImportSpecModal (1 spec URL),
NewAgent (agent name + 2 advanced number limits), Chunks (2
searches), MoveToFolderModal (new-folder pill), ConversationTile
(inline rename), ConversationBubble (tool-call denial reason),
tree/TreeBrowser (file search with rounded-t-3xl switch), and the
workflow PromptTextArea variable search.
11 raw <input> intentionally left: checkboxes (4), dropzone/file
inputs (3 - controlled by react-dropzone), the ScheduleFormModal
borderless title input (1), and the ui/input + ui/time-picker
primitives themselves (2).
Per-site className overrides keep method-row inputs tight (h-auto
rounded-lg px-2 py-1 shadow-none), borderless inline renames, and
the MoveToFolderModal primary-purple pill.
* chore(frontend): drop orphan dark-variant SVG assets (DEF.8)
Removes 5 unused color-variant SVGs that no consumer imports:
clock-purple, clock-white, monitoring-purple, monitoring-white,
white-trash. The remaining hand-tuned dark variants (documentation
pair, no-files pair, science-spark pair) are intentionally kept
because their consumers use <img> and collapsing them onto a single
currentColor SVG would require a JSX refactor outside this scope.
* fix(a11y): make AgentCard + FolderCard keyboard accessible (P5.1)
Both wrapped their entire body in a bare <div onClick> with no
keyboard handler — invisible to keyboard and screen-reader users.
Adds role="button", tabIndex={0}, Enter/Space onKeyDown, and a
focus-visible ring (focus-visible:ring-ring/50 ring-[3px]) so
the cards are actually focusable. AgentCard only takes the role
when its status is 'published' so unpublished cards stay
non-interactive. FolderCard exposes aria-pressed for its
expanded state.
* fix(a11y): add focus-visible rings to remaining outline-none sites (P5.2)
8 textareas/inputs without replacement focus visuals (PromptsModal
content, ToolConfig rename pair, NewAgent description + system
prompt, workflow PromptTextArea, ConversationBubble edit-query,
Chunks editor) now pair their outline-none with the canonical
focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:
ring-[3px] pattern. 7 callsites in ScheduleFormModal +
WorkflowBuilder upgraded from focus: to focus-visible: so the
ring only shows on keyboard focus, not on mouse click.
* fix(a11y): gate changelog banner off /share/ and /shared/ (P5.3)
The Notification banner used to render on every route as long as
showNotification was true. Embedded / public share pages
(/share/<id>, /shared/agent/<id>, /shared/...) shouldn't carry
product chrome. Adds a useLocation() check in App.tsx so the
banner only renders on application routes.
* feat(frontend): sidebar collapse -> icon rail on desktop (P6.1)
Replaces the previous "sidebar slides off-screen, only a tiny
floating header remains" collapse behaviour with a vertical
56px-wide icon rail at the left edge on lg+ viewports. The rail
surfaces the main nav actions (open-sidebar, new-chat when
applicable, search, agents, settings). Main content shifts to
lg:ml-14 when collapsed and lg:ml-72 when expanded so it never
underlaps the rail. Mobile / tablet behaviour is unchanged —
they still use the hamburger drawer.
* fix(frontend): lift changelog banner above mobile input bar (P6.2)
The Notification banner used uniform bottom-6 (24px), which on
mobile sat squarely over the Sources / Tools / Attach button row
(37px overlap into the textarea). Switches to bottom-24 sm:bottom-6
so the banner sits ~96px above the bottom on mobile (clearing the
input bar) and stays at 24px on tablet+. Now only a ~11px sliver
grazes the textarea's top border on mobile, with no controls
occluded.
* refactor(frontend): extract MessageInput sub-components (P6.3)
Splits the 1919-LoC MessageInput.tsx into 5 focused sub-components
under components/message-input/: AttachmentChipList (the file-chip
strip above the textarea), AttachFileButton (paperclip + hidden
file input), SourcesTrigger (pill + MultiSelectPopover for source
docs), ToolsTrigger (pill + lazy-loaded MultiSelectPopover for
tools), and MicButton (idle/recording/transcribing tri-state).
Parent shrinks to 1686 LoC; ~233 lines of UI logic move to 6 new
files. State stays in the parent — sub-components are props-only.
The textarea+send/stop button and the voice-capture plumbing
intentionally remain inline (would require prop-drilling /
hook-lifting beyond the conservative bar for this pass).
* docs(agents): document the icon strategy for new code (P6.4)
Adds an "Icons" subsection under the frontend coding rules in
AGENTS.md. Codifies the three current sources (lucide-react, SVG
?react components, SVG <img>) and picks lucide-react as the default
for new UI affordances, SVG ?react for brand/domain illustrations
that need currentColor theming, and discourages <img src=...> for
new icons. Notes the three remaining hand-tuned dark-variant SVG
pairs that intentionally stay paired (documentation, no-files,
science-spark) because they're multi-color illustrations, not pure
inverts.
* fix(frontend): align Settings Prompts Add button height to dropdown trigger
The Add button was rendering 10px shorter (36px) than the
Active-Prompt PopoverTrigger (46px) because the Button primitive's
default h-9 was capping it. Adds h-auto + py-3 so padding drives
the height, plus border border-transparent so the 1px border the
trigger has is counted on both — taking Add from 36 -> 46px and
closing the visible bottom-edge mismatch.
* feat: auto version release
* fix(frontend): clean up workflow-builder header duplication
The toolbar was rendering the workflow name twice (once in the
AgentPageHeader breadcrumb and once in a standalone title block)
plus the description awkwardly stacked next to the tab strip. Drops
the title block on the owner view (breadcrumb already shows the
name); the pencil edit button stays but is now always visible
instead of hidden until hover, and gains a "name — description"
tooltip so the description is still reachable at a glance. Non-
owners keep the title block since they don't see the breadcrumb.
* fix: tabs
* fix: breadcrumb and tabs
* fix: remove details btn on agent workflow
* fix: dropdown arrow
* fix: modal clicks
* fix: rounded search
* fix: mini issues
* fix: pin versions
* (chore) fe lint-fix
* fix: rounded search on source, password autocomplete, accordions in chat
---------
Co-authored-by: ManishMadan2882 <manishmadan321@gmail.com>
* Harden protection with pinned requests and path-param encoding
* fix: domain pinning
* fix: tests
* fix: html test 2
---------
Co-authored-by: Alex <a@tushynski.me>
These attributes were only set by StreamProcessor after agent creation,
causing an AttributeError in _perform_mid_execution_compression when
the context limit was hit through other code paths (e.g. worker).
Declaring them as None in init lets the handler fall through to
in-memory compression gracefully.
* feat: SSE notification system
Adds a per-user SSE pipe (GET /api/events) plus a per-message
chat-stream reconnect endpoint (GET /api/messages/<id>/events).
Backend substrate:
- application/events/ — durable journal (Redis Streams) + live
pub/sub for user-scoped events, with publish_user_event() as
the worker-side entrypoint.
- application/streaming/ — broadcast_channel for pub/sub fanout
and event_replay for the per-message snapshot+tail path.
- application/storage/db/repositories/message_events.py +
alembic 0007 — Postgres journal for chat-stream events.
- application/worker.py — ingest/reingest/remote/connector/
attachment/mcp_oauth tasks publish queued/progress/completed/
failed envelopes alongside their existing status updates.
Frontend client:
- frontend/src/events/ — connect/reconnect, Last-Event-ID cursor,
backoff with jitter. Each tab runs its own connection; no
cross-tab dedup (future work).
- frontend/src/notifications/ — recentEvents ring, cursor
tracking, tool-approval toast.
- frontend/src/upload/uploadSlice.ts — extraReducers for
source.ingest.* and attachment.* events.
Coverage: 132 SSE tests across events substrate, replay, journal,
routes, and worker publishes.
* refactor(attachments): remove polling, SSE-only
frontend/src/components/MessageInput.tsx no longer runs a 2s
setInterval against getTaskStatus for every processing
attachment. The attachment.* SSE reducers in uploadSlice.ts are
now the sole driver of attachment state transitions.
* feat(connector): consume source.ingest.* SSE, remove polling
frontend/src/components/ConnectorTree.tsx now mirrors FileTree's
slice-walking pattern: it watches notifications.recentEvents
for source.ingest.{completed,failed} envelopes matching the
sync's source id, and no longer polls /task_status every 2s.
* refactor(source-ingest): remove polling, SSE-only
frontend/src/upload/Upload.tsx and
frontend/src/components/FileTree.tsx no longer run getTaskStatus
polling fallbacks. The source.ingest.* SSE reducers in
uploadSlice.ts and FileTree's slice walk are now the sole
drivers of upload/reingest state transitions.
* refactor(mcp-oauth): carry authorization_url in SSE, remove polling
application/worker.py::mcp_oauth now publishes
authorization_url on the mcp.oauth.awaiting_redirect envelope.
frontend/src/modals/MCPServerModal.tsx consumes it from SSE
instead of polling /oauth_status/<task_id> every 1s.
The URL is generated inside DocsGPTOAuth.redirect_handler when
the FastMCP client triggers OAuth. The worker now plumbs a
publish callback through tool_config -> MCPTool -> DocsGPTOAuth
so the awaiting_redirect publish fires from inside the handler
at the exact point the URL becomes known. The legacy Redis
mcp_oauth_status setex writes and the GET
/api/mcp_server/oauth_status/<task_id> endpoint are kept as
belt-and-suspenders; nothing in the frontend reads them now.
* feat(source-ingest): plumb limited flag through SSE for token-cap UX
application/worker.py::ingest_worker and remote_worker now publish
``limited: bool`` on the source.ingest.completed envelope.
uploadSlice routes ``payload.limited === true`` to a failed status
with a ``tokenLimitReached`` flag, and UploadToast surfaces the
translated tokenLimit i18n string. No worker code path sets
limited=true today; this is a forward-looking contract so when
token-cap detection lands, the UX is already wired.
* refactor(mcp-oauth): read status from SSE journal, drop polling endpoint
MCPOAuthManager.get_oauth_status now walks the per-user SSE Streams
journal (user:{user_id}:stream) for the latest mcp.oauth.* envelope
matching the task id, returning the status string derived from the
event type suffix and the payload fields. The worker is the single
source of truth — its publish_user_event calls write the same
record the SSE client receives live.
Removed:
- /api/mcp_server/oauth_status/<task_id> route in
application/api/user/tools/mcp.py
- mcp_oauth_status worker function and mcp_oauth_status_task Celery
wrapper
- All mcp_oauth_status:{task_id} Redis setex writes (4 in mcp_oauth,
2 in DocsGPTOAuth.redirect_handler / callback_handler)
- The update_status closure in mcp_oauth that wrote the polling
payload
Tests updated:
- get_oauth_status now takes (task_id, user_id); new coverage walks
a fake xrevrange response for the completed envelope, the no-match
case, and a Redis-down case
- Removed TestMCPOAuthStatus route tests and TestMcpOauthStatusTask
celery-wrapper test
- Removed the two oauth_status methods from the integration runner
mcp_oauth:auth_url/state/code/error Redis keys remain — they are
the OAuth flow's own state (not the dropped polling payload).
* chore(mcp-oauth): delete orphaned getMCPOAuthStatus client
The /api/mcp_server/oauth_status/<task_id> endpoint was removed in
the prior commit; the corresponding userService method and the
MCP_OAUTH_STATUS endpoint constant had no remaining callers in the
frontend, so they're deleted along with it.
* fix(events): drop live publish when journal write fails
application/events/publisher.py returned an envelope to live
pubsub subscribers even when the XADD to the durable journal
failed. The envelope had no ``id`` field, which bypassed the SSE
route's dedup floor and broke ``Last-Event-ID`` semantics for any
reconnecting client.
Best-effort delivery means dropping consistently, not delivering
inconsistent state. Now: if the journal write fails the publisher
returns None and skips the live publish entirely.
* fix(notifications): dedupe sseEventReceived against immediate dupes
Snapshot replay + live tail can both deliver the same id when the
live pubsub frame and the replay XRANGE overlap. The route's own
dedup floor catches the common case, but consumers walking
``recentEvents`` (FileTree, ConnectorTree, MCPServerModal,
ToolApprovalToast) would otherwise act on the same envelope
twice when a duplicate slipped through.
Belt-and-suspenders: short-circuit when the most recent id in
the ring matches the incoming one.
* fix(events): skip replay budget INCR when no snapshot work possible
_allow_replay incremented the per-user counter on every
/api/events GET, including no-op connects from a fresh client
with no cursor against an empty backlog. React StrictMode dev
double-mounts plus a few tabs trivially tripped the default
30-per-60s budget on idle reconnects.
XLEN pre-check: when last_event_id is None and the user stream
is empty, the connect can't do snapshot work — return True
without INCR. Cursor-bearing connects still INCR unconditionally
(probing the cursor's relationship to stream contents would
require a redundant XRANGE).
* fix(streaming): tighten journal contract + recover from seq collisions
Two related fixes to application/streaming/message_journal.py.
1. record_event now rejects non-dict payloads at the gate. The
live path (base.py::_emit) wrapped non-dicts as
{"value": payload}; the replay path in event_replay synthesized
{"type": event_type}. A reconnecting client would receive a
different envelope than the one originally streamed. Now both
paths see byte-identical envelopes because non-dicts can't be
journaled at all. The corresponding event_replay fallback is
replaced with a warn-and-skip for any legacy rows.
2. record_event handles IntegrityError on (message_id, sequence_no)
collisions by reading latest_sequence_no and retrying once with
latest+1. The most likely cause is a stale seq seed on a
continuation retry where the route read MAX(seq) from a
separate connection before another writer committed past it.
Previously the error was swallowed and the event silently
dropped from the journal; now it lands at the next available
seq. The live pubsub publish uses the materialised seq so the
journal row and the live frame agree.
* perf(streaming): batch message_events INSERTs per stream
complete_stream previously opened a fresh db_session() per yielded
event, doing one Postgres INSERT + commit per chunk on the WSGI
thread. Streaming answers emit ~100s of answer chunks per response,
so the route was paying ~100 PG roundtrips per stream serialized on
commit latency.
New BatchedJournalWriter in application/streaming/message_journal.py
accumulates rows per stream and flushes on three triggers:
- size: buffer reaches 16 entries
- time: 100ms elapsed since the last flush
- lifecycle: close() at end-of-stream
Live pubsub publishes still fire synchronously per record(), so
subscribers see events in real time — only the durable journal write
is amortized. On bulk INSERT IntegrityError the writer falls back to
per-row record() with the existing seq+1 retry so a single colliding
seq doesn't drop the rest of the batch.
complete_stream wires journal_writer.close() into every exit path
(happy end, tool-approval-paused end, GeneratorExit, error handler)
so the terminal event is committed before the generator returns —
otherwise a reconnecting client could snapshot up to the last flush
boundary and live-tail waiting for an end that's still in memory.
Repository gets bulk_record() — one SQLAlchemy executemany INSERT
for the bulk path. All-or-nothing on collision (Postgres aborts the
whole batch); the writer's per-row fallback handles recovery.
* chore(upload): drop dead UploadTask.lastEventAt field
The lastEventAt field on UploadTask had no remaining consumers — the
matching Attachment.lastEventAt was cleaned up earlier. Remove the
field declaration and the slice write site.
* chore(frontend): drop orphaned getTaskStatus client
After the polling-removal sweep no caller in frontend/src/ references
userService.getTaskStatus or endpoints.USER.TASK_STATUS. The backend
route /api/task_status itself stays — agents, webhooks, e2e specs,
and the public docs still depend on it.
* docs(repo): remove stale planning docs from repo root
notification-channel-design.md, plan.md, and reminder-tool-design.md
were leftover Claude planning artifacts from the SSE substrate work
that landed accidentally. CLAUDE.md prohibits creating planning docs
unless asked — delete them.
* docs(message-events): clarify repo vs wrapper payload contract
MessageEventsRepository.record accepts any JSONB-compatible value; the
streaming wrapper record_event tightens this to dicts only because the
live and replay paths reconstruct non-dict payloads differently. Spell
the split out so the next reader of the repo method doesn't assume the
wrapper's contract applies here.
* refactor(events): raise on malformed stream id instead of lex fallback
stream_id_compare's lex-fallback branch was a footgun: a malformed id
that sorts lex-greater than a real one would pin live-tail dedup
forever, dropping every subsequent legitimate event silently. Both
current callers in application/api/events/routes.py pre-validate
inputs against _STREAM_ID_RE before calling, so changing the function
to raise ValueError is a no-op on the happy path and turns the future-
caller footgun into a loud failure.
* test(tasks): cover cleanup_message_events task body
Adds skipped-when-no-POSTGRES_URI and happy-path coverage for the
Celery janitor. The skipped path returns the documented short-circuit
shape without touching the repo. The happy path seeds a backdated
row, runs the task against the pg_conn fixture, and asserts the
retention window's row is deleted while in-window rows survive.
Mirrors the TestCleanupPendingToolState pattern.
* fix(notifications): treat /c/new as no current conversation
useMatch('/c/:conversationId') treats the literal URL /c/new as a
real conversation id, so the toast suppression check confused
'user is on /c/new' with 'user is on the conversation needing
approval'. Explicit guard: when the matched id is 'new', fall
through to the no-match case so approval toasts still surface.
* docs(events): enumerate publish_user_event None-return paths
The function returns Optional[str] today, with None conflating five
distinct outcomes (missing args / push disabled / unserialisable /
Redis down / XADD failed). Every current call site is fire-and-
forget and ignores the return, so the right move is to document the
five cases rather than promote to an enum return — keeps the API
small while making the diagnostic surface (logs) obvious. If a
future caller needs to react differently per reason, promote then.
* refactor(sources): move source-id derivation out of worker module
application/api/user/sources/upload.py imported _derive_source_id
from application.worker — pulling the entire Celery worker module
into the API process at import time just for a two-line helper.
Move DOCSGPT_INGEST_NAMESPACE and the derivation function to a
new application/storage/db/source_ids.py module that both layers
can import without that dependency edge. worker.py re-exports the
old names (_derive_source_id, DOCSGPT_INGEST_NAMESPACE) for
backward-compatible imports from tests and any other in-tree
callers; new code should import from the new module directly.
* fix(cache): enable Redis health_check_interval to surface half-open TCP
Without health_check_interval, a half-open TCP socket (NAT silently
dropped state, ELB idle-close) can leave pubsub.get_message hanging
past the SSE generator's keepalive cadence — the kernel never
surfaces the dead socket because no payload is in flight. Setting
health_check_interval=10 makes redis-py ping every 10s when
otherwise idle, so the next get_message after the dead window
raises and the SSE loop falls into its reconnect path instead of
silently freezing on the user.
* chore(events): rename attachment.processing.progress to attachment.progress
The event-type taxonomy was inconsistent: source ingest emits
source.ingest.progress (three segments) while attachments emitted
attachment.processing.progress (four segments). Drops the
.processing. infix for parity. Worker publish sites, the slice
reducer's match, and the worker tests all flip together.
No external consumers — the event type is purely internal between
the publisher and the in-tab slice; safe to rename in one commit.
* feat: events cleanup
* fix: better docs
* fix: e2e tests