mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-11-11 09:59:34 +08:00
cec874e47cd22af0735db67cd24efb149ecb2908
70
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cec874e47c | Simplify project files as artifacts (#145) | ||
|
|
07d3221f3e | Increase dashboard typography (#143) | ||
|
|
3e934765e1 |
Add Ray Jobs compute backend (v0.1.88) (#141)
* Add Ray Jobs as a first-class local compute backend. Submit via the Ray Jobs API with Settings → Compute → Ray, optional cpu/gpu/mem flavors, and the same supervise/log/cancel path as other backends. Co-authored-by: Cursor <cursoragent@cursor.com> * Review fixes: sync cloud skill, reject inapplicable flags, panic-safe log tail, fmt/clippy, dist rebuild (v0.1.87) * Round-1 review fixes: synced env for ray jobs, server-plane ray arm, help text, honest configured flag, resolver/preflight simplification, NaN guard, address validation * Round-2 review fixes: GONE debounce for purged ray jobs, cancel re-issue, prefix-guarded log tailer, drop SubmitResponse/Explicit, single URL normalization point * Round-3 nits: dirty-flag log tailer invariant, slurm-shaped cancel gate on transitions, doc accuracy * Rebase on v0.1.87; bump to v0.1.88 and rebuild ui/dist * Use official Ray and OpenResearch marks in BackendLogos --------- Co-authored-by: Sébastien Fabbro <sebfabbro@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5e25afaf98 |
Surface silently-dropped chat turns (v0.1.87) (#139)
* Surface silently-dropped chat turns (v0.1.86) A sent message could vanish with no response and no error. Evidence from the live DB: user messages persisted with no assistant reply at all — the claude turn loop waits on child events with no watchdog, so a wedged resident child (seen: expired-OAuth refresh hang) spins forever; the user's only recourse, Stop/Escape, aborted the turn task without leaving any trace; and a failed send POST was swallowed by the UI catch. - claude adapter: watchdog the event wait (120s to first event, 30min between events, matching the codex TURN_WATCHDOG), exempting turns held on a pending mcp-gate approval; on trip, kill the child and surface a visible error (next message resumes via --resume). - ChatHost::interrupt_by_user: the Stop/Escape path now persists an 'Interrupted' marker row, so a stopped turn no longer reads as 'orx did nothing'. Internal interrupts (plan resume, deletes) stay silent. - UI: a failed send now renders a local error row instead of silently clearing busy; chat SSE reconnects re-seed sessions/busy and the active transcript (chat frames are edge-only, gap frames are lost). * Address review: verified send-failure surfacing, watchdog guard arm, shared consts - send() catch: restore draft/attachments even when session creation failed; an explicit 'session is busy' rejection always surfaces (that busy is someone else's turn); a network failure probes server-side busy first and undoes the restore when the turn is real. - claude watchdog: permission re-arm is a guard arm that no longer promotes the first-event deadline; error reports minutes on the mid-turn path and the login hint only on the first-event path. - TURN_WATCHDOG hoisted to harness/mod.rs, shared with codex. - interrupt_by_user: timestamp captured before the abort; WirePart::tool constructor (also used by push_error). - Reconnect repair: shared syncSessionList (deletedIds tombstones, contextUsage merge, seenTitles baseline), seedBusy scoped by known ids, transcript reseed retries once via msgGen when a flush raced it. * Guard claude turns against --resume replay output (echo boundary) Folds in (and supersedes) the unmerged daniel/fix-swallowed-messages branch: a --resume respawn can replay prior-session/startup output before touching the new message — fatally a stale result event, which ended the turn instantly with zero parts while the real reply streamed into a dropped channel. --replay-user-messages makes the child echo the submitted message; the turn loop now skips everything before the echo. Review round (2x empirical against claude 2.1.212): echo is byte-exact and stdin-scoped (no history replay, so no same-text false positive); pre-echo lines count as watchdog proof-of-life (API backoff after boot must not be killed at 120s; only total silence keeps the fast wedge budget + login hint); pre-echo system/init harvests the session id for first-turn interrupts; module docs pin the echo contract. * Reconcile deleted sessions on SSE reconnect; repair after a failed first connect Addresses review findings 2 and 3 on PR #139: - A session deleted while SSE was down lost its chat.session.deleted frame for good: syncSessionList removed the sidebar row but left the cached transcript, loadedSessions, activeId, and — via the known- scoped seedBusy merge — a stale busy flag. Now the pre-fetch snapshot of shown sessions is diffed against the authoritative list and every missing id gets the full forgetSession cleanup, before seedBusy so the ghost's flag can't be carried forward. The snapshot is taken before the fetch (a session created mid-flight can't be tombstoned), and the project-change reset clears the snapshot ref synchronously (the old project's rows must not read as deleted under the new one). - 'reconnected' now also fires when the first successful EventSource open follows a FAILED first connect (page loaded while the backend was briefly down) — the initial fetches likely failed too, so the panel repaired itself only on later drops, never on that first recovery. A clean first open still emits nothing. Verified live on the slot-8 dev stack: session row deleted from the DB while the backend was down; on reconnect (same page, no reload) the row, ghost transcript, active selection, and busy flag all clear. |
||
|
|
bed1b31aaf |
Resolve chat file links via the files dir; fix \boxed math rendering (v0.1.86) (#140)
- FileViewer: a checkout-sourced tab whose path misses the worktree/clone now
falls back to the project's files-dir endpoints (agents link files-dir
reports with checkout-relative paths); renders via ReportMd with a note
naming the checkout that missed. Branch-ref tabs never fall back.
- Pin katex to ^0.16.22 so the imported stylesheet matches the katex version
rehype-katex actually renders with (0.16.47); the 0.18 stylesheet lacked
rules for 0.16 class names, collapsing \boxed{} to a 1px vertical line.
|
||
|
|
33feb2e1ac |
Don't mint empty reasoning parts; fix tool-group rendering artifacts (v0.1.85) (#136)
* Don't mint empty reasoning parts; fix tool-group rendering artifacts (v0.1.85) Fable's encrypted thinking streams empty thinking_delta events (payload in signature_delta) and per-block assistant events with thinking:"" — orx stored these as invisible empty reasoning parts, which split consecutive tool calls into single-row groups; an expanded singleton group then painted the identical tool line twice (summary + row). - claude.rs: skip empty stream deltas and empty thinking blocks (main loop and sub-agent routing) so empty parts are never created - ChatPanel renderParts: parts that render nothing no longer break a tool run (repairs already-stored transcripts) - ToolGroup: a single-tool group renders its ToolRow directly — no duplicate summary line; has-error color parity via styles.css - test: encrypted_thinking_mints_no_empty_reasoning_parts (captured live from the claude CLI) * Multi-tool group summary is always the count A running group is always expanded, so a summary echoing the running tool's line always sat directly above the identical row. 'Used N tools' never duplicates. Rebuilt ui/dist. * Review round: shared visibility predicate, sub-agent per-block offset fix, nits - ChatPanel: extract partIsVisible as the single invisible-part predicate, shared by messageHasVisibleContent and renderParts; renderParts now also skips resolved permission cards (Codex inlines them into the turn's parts, where they split tool runs the same way empty reasoning parts did) - SubagentTranscript: gate the empty state on rendered output, not raw part count, so stored all-invisible transcripts read as empty - claude.rs: guard empty text blocks like thinking blocks (both loops); comment accuracy passes (delta guard scope, cross-layer back-reference, ToolGroup doc, CSS child-combinator note) - apply_subagent_blocks: fix per-block offset semantics — .skip(start) treated the cumulative message offset as an index into this event's (single-element) array, silently dropping every sub-agent block after a message's first; now mirrors the main loop's start+n id shift. Caught by the new subagent_empty_thinking_mints_no_child_parts test; also added offset assertion comment |
||
|
|
5228228b2d |
Auto-generate and animate chat session titles (v0.1.84) (#135)
* Auto-generate chat session titles from the first message
Chat sessions were titled with a 64-char truncation of the first user
message. Now the first message also kicks off a short-lived headless
harness child that names the session, in parallel with the turn.
- store: new chat_sessions.title_source column ('fallback' / 'generated'
/ 'user'; NULL on legacy rows) via the best-effort ALTER pattern.
set_chat_session_title takes a source; set_chat_session_title_if_empty
becomes set_chat_session_title_if_placeholder — one conditional UPDATE
that lets a generated title replace the placeholder while a user
Rename, a legacy title, and an already-generated title all stand.
- harness: defaulted Harness::generate_title, implemented for Claude
(claude -p --model haiku --max-turns 1) and Codex (codex exec --json
at low reasoning effort). Both children run tool-less and MCP-less,
hermetically in a temp cwd, under a shared TITLE_TIMEOUT. Prompt and
output sanitizer live in the new harness/title.rs so both halves are
unit-testable; opencode and cursor keep the default.
- opencode fix: TurnCtx::set_title's if-empty check could never fire —
the placeholder write always beat it — so native opencode titles were
silently dropped. The provenance check revives them, and the seed
title opencode stamps on every new session ("New session - <ts>") is
now skipped so it can't latch and block the real one.
- chat: one emit_session helper for the re-read/resolve-busy/broadcast
sequence shared by set_archived, set_title and auto-titling; the
placeholder now takes the first non-empty line.
No UI changes: the existing chat.session SSE upsert re-renders Recents.
* Animate the reveal of auto-generated session titles
An auto-title used to pop into the sidebar with no acknowledgement that
anything happened. Now it materializes character by character.
- session_json gains "titleSource" so the UI can tell a harness-generated
title from a placeholder or a user rename.
- New TitleReveal component, used by both the Recents row and the chat
header so the effect is identical in each. Characters are aria-hidden
behind an aria-label — a screen reader hears one title, not forty
single-letter spans — and render as a plain string when not animating.
- The reveal plays only on a live chat.session event that changes a title
we already had on screen, and only for titleSource "generated": renames
and page loads swap silently. A per-session nonce remounts the spans so
a second retitle replays rather than sitting on a finished animation.
- CSS: 240ms per character, 14ms stagger capped at 500ms, fading in from
blurred and slightly below. No color, no scale, no layout shift, and
@media (prefers-reduced-motion: reduce) turns it off entirely.
* Trim the skills catalog from codex title requests
The plugin/skills catalog injected ~6k prompt tokens (24.3k -> 18k input
measured) into a one-line title request that ignores it.
* Strip the agent system prompt from claude title requests
--system-prompt replaces the ~8.5k-token Claude Code scaffolding with one
sentence (measured 8.5k -> 1.9k input tokens; wall time unchanged at ~3s,
which is node boot + one API round trip).
|
||
|
|
1b9b62328f |
Agent | Project scope toggle for the Experiments pane (#134)
* Agent | Project scope toggle for the Experiments pane (v0.1.83) Attribute experiments to the chat session that created them (local_experiments.chat_session_id, stamped from ORX_CHAT_SESSION_ID like runs, immutable after creation) and add an Agent | Project toggle to the experiments pane. Agent scope filters the tree to the open session's experiments, collapsing foreign lineage regions into dashed "…" pills with counts, and scopes the runs table to the same experiments. * Show the Agent | Project toggle only in projects with attributed experiments Legacy projects (created before chat_session_id existed) have no attributed nodes, so the toggle would only ever show an empty Agent view there. Gate it on the project having at least one attributed experiment — no backfill needed, and pre-existing projects keep today's UI untouched. * Require full attribution before showing the Agent | Project toggle Any unattributed experiment (legacy or pre-feature) makes Agent scope misrepresent the tree, so the toggle only renders when every experiment in the project carries a chatSessionId. |
||
|
|
397dd48815 |
Stream Codex sub-agent activity into the chat transcript (#117)
* Stream Codex sub-agent activity into the chat transcript Sub-agents spawned via Codex collaboration now stream their own transcript live, nested under the spawn row, instead of showing an opaque collabAgentToolCall/subAgentActivity row. - WirePart gains a recursive children list (serde-default, no migration) - apply_item split into a pure item_to_part + thin streaming wrapper - turn loop classifies foreign-turn events: parent / known sub-agent / stale, routing sub-agent items+deltas into the spawn part's children; the aborted-predecessor drop is preserved - UI renders the nested transcript inline (recursive, auto-expands while running) plus a right-pane view tab * Address review: settle orphans on all exits, re-point sendInput, share tool-group CSS, trim spawn fields * Nits from review round 2 + version bump to 0.1.77 - drop dead .subagent-block class token - re-point grandchild threads on sendInput (symmetry with top-level) - fix stale doc comment on subagent_spawn_part - rebuild ui/dist, bump Cargo version * Fix un-collapsible running sub-agent row; add live header to view tab - SubagentBlock: expanded is now a user-toggleable state seeded from running, instead of `open || running` which made a running row impossible to collapse - SubagentTranscript: show a header (spawn label + status dot + "live" badge) so the right-pane tab reads as the sub-agent's own live view, not a duplicate of the inline block * Stream Claude and OpenCode sub-agent activity too Extends live sub-agent streaming (shipped for Codex) to the other two harnesses, so a Task/task sub-agent's transcript nests under its spawn row in all three. - Promote find_part_mut / upsert_preserving_children to chat/mod.rs and add TurnCtx::upsert_child / append_child_text — shared routing primitives - Claude: route stream_event/assistant/user events carrying parent_tool_use_id (= the Task tool_use's part id) into that Task part's children, instead of dropping the deltas and leaking the blocks flat - OpenCode: link a task-spawned child session (session.created carrying parentID) to its task row, and route that child session's parts/deltas into the row's children instead of filtering them out by sessionID - UI: a tool part with children renders as a spawn block regardless of tool name (Codex subagent / Claude Task / OpenCode task) - Tests: per-harness nesting tests; rebuilt ui/dist * Review fixes: preserve spawn-row children on re-upsert; filter sub-agent user parts - OpenCode final-message merge and Claude Task tool_use now use upsert_part_preserving_children so completing/re-emitting a spawn row doesn't wipe the sub-agent transcript that streamed into its children (blocker) - OpenCode: gate sub-session parts on owned_by_assistant so a child session's user prompt echo doesn't nest as sub-agent output; track sub-session assistant message ids - tests: assert Task/task row completes AND keeps children through the merge * Nits: use imported HashSet; document one-level nesting scope in OpenCode * Sub-agent row opens transcript in the right panel instead of expanding inline Per design feedback: the inline sub-agent spawn row is now a single clickable line (status dot + label + chevron) that opens its full transcript in the right-side panel — matching the Claude/Codex desktop apps. No inline expansion, no auto-expand while running (the row just pulses). Nested sub-agents in the panel are themselves click-to-open rows. * Size the sub-agent row like the transcript body text Match the spawn row's font-size (--fs-base) and color (--text) to the surrounding transcript prose, keeping the trailing chevron. * Sub-agent row: block-level flex so long labels truncate with ellipsis Review nit: inline-flex + flex:initial let a long label grow the row and push the chevron instead of truncating. Use block-level flex + width:100% and keep the base .tool-line flex:1/min-width:0 so the label ellipsizes and the chevron pins right (matching .tool-group-summary). Also refresh a stale doc comment. |
||
|
|
17633be723 |
Add a hover detail card to experiment tree nodes (v0.1.83) (#133)
* Add a hover detail card to experiment tree nodes (v0.1.83) Hovering a node in the experiment tree (350ms intent delay) opens a portal-anchored card with the full title/description, run stats (counts, backend, duration), a lazily fetched diffstat vs the parent branch, branch lineage, and the run command. Portaled to <body> so it neither scales with canvas zoom nor disturbs the fixed tree layout; dismissed on mouse-out or canvas wheel. * Review round 1: shared diff parser, viewport-change dismissal, honest counts - Parse the diffstat with react-diff-view's parseDiff + the Changes tab's countChanges/formatDiffFilePath (now exported) instead of a hand-rolled scanner; truncated diffs render as lower bounds. - Replace the global wheel listener with useOnViewportChange dismissal (covers drag-pan, pinch, fitView) and re-measure the anchor when tree data changes; position via the existing useMeasure/usePopoverPosition. - Fix duplicate failure text, 'passed'→'done' wording, reconciled run counts, live-run duration fallback, mono font-size leak, z-index tier, and reuse diff-stat-add/del classes. * Review round 2: reliable pan/zoom dismissal, truncation-safe parsing, side fallback - useOnViewportChange stores a single global callback, so per-node registrations overwrote each other; dismissal now flows from ReactFlow's onMoveStart through a module-level EventTarget every open card subscribes to. - Byte-capped diffs can cut mid-hunk and crash parseDiff; trim to the last complete file (or line) first, try/catch as a backstop, and hide the row for empty diffs. - On windows too narrow for either side placement, anchor above/below instead of letting the clamp sit the card on top of its own node. - Skip rect churn on SSE ticks that don't move the node. * Review round 3: reset diffstat on run change; label truncated file count The diffstat effect never cleared state, so when SSE flipped latestRun while a card was open the old run's counts lingered beside the new run's status (and a failed refetch kept them forever). Reset at the top of the effect. Also mark the file count as a lower bound on truncated diffs and align the cancellation-flag name with the codebase idiom. * Skip header-only diffstat remnant after truncation trim * Hover card feedback: expandable description, darker text, drop file names - Clamped descriptions get a Show more / Show less toggle; expanded text scrolls within the card instead of growing past the viewport. - Title, stats and git rows read in --text instead of --muted (the card was mostly grey); the foot row stays muted as tertiary metadata. - Diffstat keeps branch and +/− counts, drops the changed-file list. * Drop the description's recessed background for hairline rules The filled box was the only non-hairline surface on the card and read as a heavy slab on long results; the description is now plain text between the same hairline rules the rest of the app uses. * Post-feedback review nits: toggle hygiene and hairline polish Reset the expanded state when SSE swaps the description; type=button on the toggle; trim CSS the global button reset already provides; bottom padding on the expanded scroll body; comment accuracy. |
||
|
|
04cae09219 |
Auto-grow the chat composer with multi-line input (v0.1.82) (#132)
* Auto-grow the chat composer with multi-line input (v0.1.82)
The composer textarea was fixed at two rows (rows={2}, resize: none), so
multi-line drafts scrolled inside a tiny box instead of expanding it
(issue #130). Grow it to fit content up to the existing 180px max-height
cap, past which it scrolls internally; an empty draft clears the inline
height so the resting two-row look is unchanged (and Chrome's
placeholder-in-scrollHeight quirk can't inflate an unsettled layout). A
width ResizeObserver re-measures on pane resizes since wrapping changes
the needed height.
* Rebind the autosize observer on composer remount; review nits
The composer textarea unmounts when a settings view opens (mainView !=
chat renders a different tree); the effect-based observer kept watching
the detached node and a remounted draft came back at two rows. Replace
the effect with a stable ref callback that rebinds the ResizeObserver
and re-measures on every mount. Also: skip measuring at zero width
(hidden/mid-layout scrollHeight is nonsense), drop the circular
useCallback around autosizeComposer, and document the width-only guard,
the CSS min/max-height coupling, and the field-sizing tradeoff.
* Floor the composer at its rows=2 resting height; comment polish
A one-line draft measured ~33px and clamped to the old 42px min-height,
so the box dipped ~10px on the first keystroke and jumped back when
cleared. Raise the floor to the rows=2 resting size (52px) so empty and
one-line drafts sit at the same height. Also soften the ref-callback
comment (an effect could work with a mount flag) and note the plan-strip
textarea sharing the CSS rule.
* Replace the JS autosize with CSS field-sizing: content
Drops the whole autogrow block from ChatPanel (measure function, layout
effect, ref callback, ResizeObserver) for one CSS declaration. Firefox
doesn't ship field-sizing yet and falls back to the previous fixed
two-row box; min-height stays 52px so the resting look is unchanged, and
the plan strip's revise textarea picks up the same growth for free.
|
||
|
|
62609c020b |
Don't treat single dollars as inline math in chat/report markdown (v0.1.81) (#131)
Prose like "cost $8–18 across both nodes ($1.45 each)" was pairing its dollar signs into a KaTeX inline-math region, italicizing the span and collapsing spaces. remark-math's default singleDollarTextMath treats any $...$ as math; disable it and normalize agent-emitted \(...\) to $$...$$ so real math keeps rendering. |
||
|
|
30e2ab18f4 |
Make reasoning/effort choices model-aware (#123) (#127)
* Make reasoning/effort choices model-aware (#123) The composer exposed one static reasoning list per harness, which hid valid choices and silently overrode the CLI's own configured effort. Reasoning choices now ride on each ModelInfo, with the harness-wide list kept only as the fallback for a session with no model selected: * Codex — per-model tiers from a curated table (codex exposes no queryable effort metadata), kept in the same table as the model ids so a model can't be added without stating its tiers. Verified live against codex-cli 0.144: all three 5.6 models accept `ultra`, and 5.5 rejects `max` with a 400 — issue #123's table claims Luna stops at `max`, which did not reproduce. This matters because codex forwards the value unvalidated, so a wrong tier kills the turn. * OpenCode — real per-model `variants` parsed from `opencode models --verbose`, sent as a top-level `variant`. A model with an empty variants map hides the picker entirely, which is why absent and empty are distinct states. Unknown variants are forwarded rather than filtered: the catalog is the authority and already ships ids outside our vocabulary (`thinking` on minimax-m3). * Claude Code — `ultracode` gated on CLI >= 2.1.217. The installed 2.1.197 does not error on an unknown effort; it warns and silently runs at the default, so advertising it unconditionally would offer a choice that does nothing. Every list leads with a `default` sentinel that sends no override at all, and it is the default selection — so a configured `model_reasoning_effort = "max"` is no longer clobbered by an implicit per-turn `high`. Switching models reconciles a stored effort the new model doesn't support back to `default`, both in the composer and again in each harness mapper before it reaches a CLI. Deliberately no store migration for existing rows: every value older builds wrote is still a value the picker offers, so a blanket reset would be indistinguishable from a level the user just chose — and Store::open() is a per-request constructor, so it would re-run and clobber fresh picks. * Lower the ultracode gate to 2.1.203, the measured boundary The constant was 2.1.217 — the version issue #123 happened to report — with a comment admitting the true cutoff was somewhere in (197, 217] and that lowering it needed a version actually tested. It has now been tested: every published release in 2.1.197..=2.1.212 was installed in isolation and probed. 2.1.198-2.1.202 reject (warn on stderr, run at default effort); 2.1.203 and every version above accept. So a 2.1.212 install was being denied a choice its CLI supports. The acceptance is specific to this value rather than newer CLIs dropping the warning: on 2.1.206, `ultra`, `ultrathink`, and `ultracode123` all still warn while `ultracode` does not. `--help` lists only the five ordinary tiers on every version tested, so it can't be used to detect support. The tests now pin the 2.1.202/2.1.203 pair, so moving the constant without re-probing fails loudly. * Discover the Codex catalog from the CLI instead of curating it The model list and per-model effort tiers were hard-coded. That was wrong on the facts, not just the principle: the installed codex actually serves seven models (gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark were missing), and the curated Luna entry advertised `ultra`, which the catalog does not offer — codex's own picker doesn't show it, so ours shouldn't either. (An earlier live `codex exec` turn on Luna tolerated `ultra`, which is what the table was built from; the catalog is authoritative for what to offer.) Detection now spawns `codex app-server`, does the initialize handshake, and calls `model/list` — the same data codex's TUI picker renders, with each model's `supportedReasoningEfforts`. This is the codex analogue of the opencode `models --verbose` path. The static table survives only as the fallback for a codex too old to answer (legacy exec era), with Luna corrected to the catalog's tiers. Send-time validation follows the opencode precedent for catalog-discovered models: a model outside the frozen fallback table forwards its level rather than clamping to the conservative intersection, because the composer only offered what `model/list` reported — clamping would drop genuinely supported tiers. A bad value still surfaces as codex's 400 in the chat. Claude stays gated rather than discovered, now with evidence there is nothing to query: no models command, a static `--help`, and the "Valid values" warning on 2.1.212 still omits `ultracode` even though that version accepts it — the CLI's only enumeration surface is inconsistent with its own parser. * Discover the Claude catalog and ultracode support from the CLI Claude Code was the last harness with hard-coded models and effort tiers. It turns out the CLI can be asked for both, via two surfaces: * `list_models` — a control request over `--print` stream-json (the channel run_turn already speaks for set_model), returning the same catalog the CLI's own /model menu renders: each model's alias, and its per-model `supportedEffortLevels`. This immediately corrected the static list a third time: Haiku supports no effort control at all, yet the table had been offering all five tiers on it. Its picker now hides, like a variant-less opencode model. * `--effort ultracode --version` — the CLI's own argument parser as a capability probe (~0.2s, no network). `--version` still runs the parser, which warns `Unknown --effort value` for a value it doesn't know; absence of the warning is acceptance. This replaces the bisected version constant entirely: the 2.1.202/2.1.203 boundary is now discovered per install rather than pinned. The parser is the only truthful surface — --help, the warning's own "Valid values" list, and list_models all omit ultracode on versions that accept it, because the CLI models it as a session mode, not an effort level. It is appended to models that reach xhigh (the mode is documented as xhigh + dynamic workflows), only when the probe passes. claude_effort now forwards any non-sentinel value, matching the catalog policy in codex_reasoning and opencode_variant: the composer only offers what the CLI reported, an allowlist would drop a future catalog tier, and Claude only warns-and-defaults on an unknown value rather than failing the turn. The static model/effort tables survive only as the fallback for a CLI too old to answer list_models, mirroring the codex and opencode fallbacks. * Carry the catalogs' display names and blurbs into the picker The Claude aliases the CLI's picker submits (`opus[1m]`, `sonnet`) are unversioned, so the model menu couldn't say which Opus or Sonnet you'd get. The catalogs already answer this: list_models has displayName + description (the description is where the resolved version lives — "Opus 4.8 with 1M context · …"), codex model/list has the same pair, and opencode's verbose output has name. ModelInfo now carries display_name/description, and the picker renders the catalog's own strings — the same text each CLI's native picker shows — with the raw id demoted to a hover title. Statically-listed fallback models keep deriving a label from the id. * Lead with versioned names, drop the Default row, allow free-form model ids Three picker fixes from using it: * The Claude rows read "Fable / Fable 5 · Most capable…" — the name repeated because the catalog's displayName is unversioned and the version lives in the description's first `·` segment. That segment is now promoted to the display name ("Fable 5") with the rest as the blurb, so rows lead with the resolved version, exactly once. * The per-harness "Default model · CLI configuration" row is gone wherever the CLI advertises a catalog — it was redundant noise next to a discovered list that already leads with the CLI's default. It survives only for custom providers, which advertise no models and where "let the CLI decide" is the only sensible row. New selections seed the catalog's first model instead of null. * Typing an id the catalog doesn't list offers "Use "<id>"". The catalogs are curated menus, not the set of accepted ids — `--model claude-opus-5` works today on a CLI whose menu still resolves `opus` to 4.8 — so the menu can't be the ceiling. A custom id gets no per-model effort list (unknowable), so the reasoning picker falls back to the conservative harness-wide tiers. * Preselect the effort that actually runs instead of a Default sentinel The reasoning menu led every list with "Default · CLI configuration" — a pseudo-value that told the user nothing about what would run. Replace it with the real answer wherever the CLI can give one: * Codex reports each model's defaultReasoningEffort, and config.toml's model_reasoning_effort outranks it (codex's own resolution order — verified: this machine's config sets "high" and the picker now preselects High on every model, not the catalog's low/medium). The sentinel row is gone for catalog models; the resolved tier is preselected and marked "· Default". Sending it explicitly is equivalent to sending nothing, by construction. * Claude reports no default tier because its unset default isn't one: the catalog advertises supportsAdaptiveThinking, and with no --effort the CLI scales effort per request. Preselecting a fixed tier would pin behavior the user never chose, so the sentinel stays — now labeled "Adaptive", which is what actually runs, instead of "Default". * OpenCode reports no per-model default, so its sentinel row stays as is. reasoningFor resolves in that order: the model's reported concrete default, then the sentinel when offered, then the harness-wide default. Stale stored "default" sentinels on codex sessions reconcile to the resolved tier. * Render a concrete default tier inline instead of pinning it Pinning above a separator now applies only to the `default` sentinel, which isn't a point on the tier ramp (Claude's Adaptive, opencode's model default). A concrete default — codex's resolved tier, a permission mode — stays in its natural ramp position with just the "· Default" marker, so the codex menu reads Low → Ultra in order. Side effect on the shared mode menu: Auto renders inline with the marker and the 1/2/3 number hints cover the full list again. * Make every picker change update the sticky new-session preference The mode/model/reasoning selection saved for new sessions only updated when the pickers were changed with no session open. A change made inside a session landed as that session's unsent tweak and the global preference never learned about it — so configuring auto/sonnet/high mid-session and clicking New session showed the old config. selectModel now always writes the merged selection to the sticky preference (state + localStorage), and additionally layers it onto the open session's override as before. New session therefore opens with whatever was chosen last, wherever it was chosen — verified across a full page reload. |
||
|
|
07a7e209ff |
Detect Codex and Claude Code behind custom API endpoints (#125)
Follow the active provider in $CODEX_HOME/config.toml so a provider with requires_openai_auth = false authenticates off its declared env_key, and offer the model it configures instead of the first-party catalog. Accept ANTHROPIC_AUTH_TOKEN for Claude Code, letting the env credential win over a stale ~/.claude.json only when ANTHROPIC_BASE_URL is set. Add a "Default model" picker entry that sends model: null so the CLI uses its own configured default. Closes #121 |
||
|
|
979d8e9362 |
Rework first-run onboarding and the New project modal (v0.1.79) (#126)
* Tighten the onboarding + new-project flows
Onboarding:
- Gate steps 1 and 2 (a signed-in agent, GitHub access) — both are hard
requirements for chat and project creation, so Continue is disabled
until they're met.
- Drop the data-dir step: the default suits ~everyone, it's write-once,
and Settings > Storage already exposes it with a move flow.
- Split harness notes into not-installed vs not-signed-in, each with the
real command (claude auth login / codex login / opencode auth login).
- Fix OpenCode reporting Connected while unauthenticated (agent_ready
ignored authentication).
New project modal:
- Auto-open on the onboarding hand-off.
- Only show the fork choice when the user can actually push (new
/api/github/repo-access), and say what happens when they can't.
- Name the real GitHub account in repo previews (new /api/github/account).
- Drop the branch picker; resolve the repo's real default branch server
side instead of assuming "main".
- Describe the copy honestly: it's a single-branch, history-less snapshot,
not a GitHub fork.
- Top-anchor the modal so the tab row doesn't move between tabs.
* Review round 1: fix onboarding lockouts, trapped name field, stale fork mode
- opencode: treat a non-empty `opencode models` list as authenticated, so
env-key installs (no auth.json) aren't locked out by the new step-1 gate.
Claude and Codex already had the equivalent fallback.
- Onboarding: surface a load error. A rejected probe left harnesses/git at
null forever, which the gates read as not-ready — disabling Continue with
no hint and no way out, on the screen that is the whole app at first boot.
- NewProjectForm: hoist the project-name field out of repoFields. A stalled
access check used to unmount it, and `valid` needs a name, so the form
became unsubmittable with no escape.
- NewProjectForm: reset canPush on every repo change and sync repoMode to
the answer, so a forced "fork" can't linger and silently copy a repo the
user can push to.
- github.rs: share one authed_get helper (with a 10s timeout — there was
none), URL-encode owner/repo, drop the now-dead has_push_access wrapper.
- Drop the unread `known`/`defaultBranch` fields from the repo-access
response and its TS type.
- ProjectsHome: consume the post-onboarding hand-off so a later remount
can't re-open the modal.
- Deduplicate the paper-repo hint and the two identical code-chip CSS rules;
tie the modal offset and max-height together with a --modal-top token.
* Review round 2: correct the opencode fallback, unblock SSH-only users
The round-1 opencode fix rested on a false premise — I claimed `opencode
models` only lists authenticated providers. Tested: under a signed-out HOME
it returns nothing, so the fallback never fired for the users it was meant to
rescue, and had the catalog ever been listed it would have marked signed-out
installs Connected. Replaced with an explicit provider env-key check, matching
claude.rs. Verified both directions: env key set -> ready, none -> gated.
- Onboarding: step 2 gets a "Skip — I use SSH keys" escape. ensure_clone
tries ssh first, so a token isn't actually required for the use-existing
path, and git_settings_json can't see SSH keys at all — the hard gate was
locking out a setup the CLI's own startup note calls supported. Also covers
the git-not-installed dead end, where the token form is unreachable.
- Onboarding: split the shared loadError into per-probe flags. A telemetry
failure was putting a connectivity error on unrelated gates and hiding the
actionable hint behind it.
- NewProjectForm: only force "fork" on denied access; the symmetric sync was
undoing selectPaper's deliberate copy default for writable paper repos.
Reset repoMode in onRepoChange instead, which is where the repo changes.
- github.rs: drop the deleted has_push_access's doc comment, left welded onto
authed_get and describing the wrong thing.
- Bump to v0.1.79 (+ Cargo.lock): ui/dist changes don't ship without it.
* Review round 3: detect synced-env keys, unstick the branch fallback
- Harness detection: resolve API keys through orx's synced env file as well
as the process env (new detect::api_key, used by claude.rs and opencode.rs).
A key pasted into Settings > Environment is handed to the harness child, so
chat works — but detect() couldn't see it, and step 1 is a hard gate.
Verified: key only in ~/.openresearch/env now reports ready for both.
- opencode: widen the provider key list (Gemini/Groq/xAI/DeepSeek). Measured
rather than assumed that `opencode models` can't stand in for a credential:
signed out with no keys it still lists 7 free/bundled models, so keying off
it would report signed-out installs as Connected.
- baseline_branch: fall back to `git ls-remote --symref` when the GitHub API
can't answer. Without a token the branch resolved to a hardcoded "main" and
the clone died on any master/dev repo — and the form no longer has a branch
field to recover with. git's own ssh/https credentials need no token.
- Step 1 no longer gates on a failed probe: detection fans out to per-binary
version/model commands that can stall, and there's no way out of onboarding.
- Step 2's skip is always offered and reworded "Skip for now": a stale gh
token still reads as connected, hiding the escape from the users who need
it most. Promoted to a real button rather than a muted link.
* Rebuild ui/dist after rebase
* Review round 4: stop 404 forcing a history-losing copy
- repo_meta: a 404 means "invisible to this token", which is also how a
private repo looks to an SSH-only user who can push to it fine. It was
reported as can_push=false, which force-forks — and this branch removed the
"Use this repo" button in that state, so the override was gone too. The
user would silently get a new repo with their history dropped. Report
unknown instead, matching the fallback used everywhere else.
- remote_default_branch: bound it with a 15s timeout. ls-remote tries ssh then
https, and a black-holed host blocks on TCP connect ~75s each — the exact
hazard authed_get got a timeout for last round, reintroduced on the git path
of the same request. A stall now degrades to "main" instead of hanging.
- git.rs: the new function had been inserted between ensure_clone's doc
comment and ensure_clone, so the comment described the wrong function and
ensure_clone was left undocumented.
- Never show a "Detecting…" spinner next to a fetch error: on a failed probe
both steps now render the retry copy in place of the card, instead of a
spinner that claims work is in progress beside an enabled Continue.
- onRepoChange no longer resets repoMode in paper mode, where editing the
auto-filled repo would quietly retarget pushes to the upstream paper repo.
* Review round 5: make step 2 an honest soft gate, fix stale-state contradictions
- Step 2 no longer pretends to gate. Cloning and pushing work over SSH keys,
so a disabled Continue beside an always-enabled "Skip for now" was two
buttons doing the same thing — one greyed out, which reads as a bug. One
Continue, always enabled, and the hint states what's missing instead of
blocking on it.
- A rejected probe now clears its state as well as setting the error flag.
Otherwise a failed *re-check* kept the old cards on screen: stale "Not
signed in" badges and a "sign in to continue" hint next to a Continue that
harnessError had just un-gated, with no indication the check had failed.
- Mode switches reset repoMode via a chooseMode helper. Leaving paper mode
carries the paper's repo into the field but not its copy default — and
because the field is pre-filled, onRepoChange never fired to reset it, so
"Private copy" stayed selected on a repo the user never chose it for.
- Deduplicate the project-name field and blank-repo hint, both of which were
inlined a third time in "New blank repo" (flagged since round 2).
* Review round 6: restore the Existing repo name field, kill the footgun
Round 5's nameField refactor turned it into a function but updated only two of
three call sites. `{parsed && nameField}` passes the function itself as a
React child, which renders nothing — so "Existing repo" had no Project name
input and Create stayed disabled. Both tsc and a grep of the call sites missed
it; three reviewers caught it.
Made the class of bug impossible rather than just fixing the call site: the
autoFocus parameter was the only reason nameField was a function, so it's a
plain element again like blankRepoHint. Verified in a browser this time —
name field present and submit enabled in every mode.
- Onboarding: the docstring still called step 2 a hard gate, which round 5
made false, and it sat below the RETRY_COPY const so it documented the wrong
symbol. Rewritten and moved.
- Onboarding: sequence-guard load(). A token save landing mid-refresh was
overwritten by the pre-save snapshot, leaving the card reading "Not
connected" until the next check; the save now supersedes in-flight probes.
* Review round 7: fix a Re-check lockout, share renderNote, trim comments
- load()'s .finally was sequence-guarded, so a token save (which bumps loadSeq
to supersede the in-flight probe) left `checking` true forever and both
Re-check buttons dead for the session. `checking` is "a load is running",
not data a stale response can corrupt — it shouldn't be guarded. Introduced
by round 6's guard; verified fixed by superseding a slow load in a browser.
- renderNote moved to its own module: ModelPicker, a core chat control, was
importing it from the first-run walkthrough.
- Drop the redundant {parsed && …} guard (repoFields already returns null),
a doubled CSS comment block, and two stacked rationale comments.
- Document the opencode agent_ready tightening where the behaviour changes:
installs with no auth.json and no provider key now read "Not signed in".
|
||
|
|
f84bdf3b75 |
Catch "Permission denied (publickey)" before it costs a box (v0.1.78) (#119)
* feat: catch "Permission denied (publickey)" before it costs a box orx shells out to ssh with no -i, so it authenticates with whatever the local agent and ~/.ssh offer, while boxes authorize the keys registered on the account. A key registered from another computer leaves the box online but unreachable, and the user only finds out after paying to provision it. Compare the registered public keys against the locally-available ones at login, before launch, and on a failed launch, and add `orx ssh-key add|list` — the command the dashboard has always pointed at but which never existed. - login offers to register this computer's key ([Y/n], TTY-gated) - preflight blocks when nothing is registered, warns when we can't find a local match (ssh can still succeed via an IdentityFile elsewhere) - a publickey denial becomes one actionable line in `orx runs` - the settings badge distinguishes "on this computer" from "registered elsewhere", which the old boolean could not * fix: don't call the openresearch backend "Configured" without a usable key The compute row keyed "configured" off being signed in, so it showed a green badge on a backend that would refuse every connection — the same gap the launch failure exposed, one screen earlier. Gate it on holding a key the account has registered, and say which of the two things is missing. Also run the check when openresearch is made the default, since that's the moment the user is committing to it. Unknown (api unreachable, ~/.ssh unreadable) counts as ready: we couldn't tell, so don't cry wolf. * fix: say "Unknown" when the readiness check couldn't run Folding a failed check into "configured" put a green badge on a backend we never verified; folding it into "not configured" would show alarm whenever the api hiccups. Neither is true, so stop making the boolean answer a question it has no data for. Adds an optional `unverified` flag — absent for backends whose state is decidable locally, so no other row changes. * fix: name the key file we found instead of guessing one The compute row told everyone to run `orx ssh-key add ~/.ssh/id_ed25519.pub` — a path that doesn't exist on a machine whose key is named anything else, so the fix led to a second error. The readiness check already resolves the real file via preferred_local; carry it through instead of discarding it. Split the keyless case out, since `ssh-key add` alone can't help there, and surface the Unknown reason so that state stops being a dead end. * chore: v0.1.78 * fix: stop guessing ~/.ssh/id_ed25519.pub in the skill, notes and run failure Same bug as the compute row, in three more places. The skill was the worst of them: an agent reads it and runs the literal command, so on any machine whose key is named differently it fails with nothing to go on. - skill: tell the agent to read the path from `orx ssh-key list` rather than handing it one, and cover the no-key-at-all case - settings notes: use the .pub the check already found (new sshKeyPath in the payload), with separate copy when there's nothing to register - run failure: point at `orx ssh-key list`; the supervisor can't see ~/.ssh, so it has no path to name |
||
|
|
fcba6a25d1 |
Show a loading state instead of the empty state while chat history loads (v0.1.77) (#118)
* Show a loading state instead of the empty state while chat history loads (v0.1.77) Switching between agent conversations briefly flashed the empty state (OpenResearch logo + 'Ask the agent to explore your codebase...') before the conversation's messages finished fetching. The empty vs transcript branch was gated purely on messages.length, which can't distinguish a not-yet-loaded session from a genuinely empty one. Add a distinct 'Loading conversation…' state, driven by key presence in messagesBySession (absent = still fetching). A failed fetch now seeds an empty transcript so it recovers to the usable empty state instead of a stuck spinner, and drops the loadedSessions guard so a switch-back retries. * Enlarge the chat loading spinner and text for legibility The 13px muted text + 13px wheel read as too small and hidden. Bump the wheel to 22px (thicker stroke), the label to 16px, and use --subtext for better contrast. * Guard failed-fetch recovery against clobbering streamed messages Review nit: the .catch seeded [] unconditionally, which could overwrite a message that streamed in via SSE during a failed history fetch (self-heals on the next re-broadcast, but briefly drops it). Add an onlyIfAbsent flag to the seed action so recovery no-ops when the transcript key already exists. |
||
|
|
7940344d44 |
Open agent-inlined files-dir report links from chat (v0.1.76) (#116)
Clicking a report.md link an agent inlined into the chat transcript failed with "path must be repo-relative": the report lives in the files dir (<data dir>/files/<slug>/…), but every chat file-open routed through parseFilePath → FileViewer → the repo /file endpoint, which rejects absolute paths. parseFilePath had no files-dir case and FileViewer carried no source discriminator, so files-dir artifacts were unroutable. - Backend surfaces a (non-canonical) absolute filesDir on every project payload via a shared project_json helper, routed through all six emission sites including the SSE project.updated diff — enriching only list_projects would let a bare project clobber filesDir on the next visit. - parseFilePath gains a files-dir branch: exact-prefix match against filesDir (byte-for-byte with the path the agent inlines), plus a /files/<slug>/ suffix-regex fallback for symlink-divergent data dirs. Tabs it produces carry source:"files". - FileViewer branches its fetch by source: files-dir reports via /files/report, other files via /files/file, else the repo endpoint. Files-dir markdown renders through FilesTab's ReportMd (now exported) so report-relative image paths resolve; a missing file shows a friendly not-found; repo-only notes are suppressed. Verified end-to-end: the failing report.md chip now opens the report from the files endpoint with no error. |
||
|
|
7956d54af2 |
Per-session context-window usage meter (#114)
* Per-session context-window usage meter
Surface how much of the model's context window each chat session has used —
a compact percent pill beside the model picker, with a popover breaking it
down into a progress bar and token caption (amber >=80%, red >=95%).
Backend: harness adapters now parse the token usage the CLIs already emit and
discard today, normalize it into a ContextUsage (used = latest request's
input + cache + output, not cumulative), stream it live over a chat.usage SSE
event, and persist the final value to a new chat_sessions.context_usage_json
column so it survives restarts. session_json carries contextUsage thereafter.
- Claude: assistant message.usage (live) + result modelUsage.contextWindow
(window), falling back to the last usage.iterations entry.
- Codex: app-server turn/completed usage and legacy exec token_count
(input + output; cached_input is a subset). Window from model_context_window
when reported; static fallback intentionally empty (no invented numbers).
- OpenCode: assistant message.updated tokens (input+output+reasoning+cache);
window left unknown (provider config only), so the pill shows the count.
Frontend: ChatSession.contextUsage + fmtTokens helper; a chat.usage listener
that updates the session in place; a ContextMeter pill/popover; and a shared
ProgressBar extracted from SettingsPage and reused in both.
ui/dist rebuilt. A release version bump is still needed to ship.
* Restyle context meter: ring trigger + desktop-style popover
* Context meter: darker value text, more gap before send
|
||
|
|
cef4881032 |
Stop composer keystrokes from re-rendering the whole transcript (v0.1.74) (#113)
* Stop composer keystrokes from re-rendering the whole transcript Typing in the composer was laggy on long conversations: the draft state lives in ChatPanel, which also rendered the transcript inline, so every keystroke re-ran the full component body, re-filtered every message, and allocated + memo-compared all N Message elements (doubled by StrictMode in dev). PR #111 fixed the streaming path but left the typing path. - Extract the transcript map into a module-level memoized Transcript component; all its props are referentially stable across keystrokes, so typing now costs one shallow compare instead of O(messages) work. - Memoize the waiting-session scan (ran per session row per render, iterating messages x parts) into a useMemo'd Set keyed on reducer state, which only changes on SSE dispatches. * Review nits: mirror Message's onRespond type, state memo constraint * Ship rebuilt ui/dist; bump to v0.1.74 |
||
|
|
9fe3579c2d |
Stop re-rendering the whole transcript on every chat streaming tick (v0.1.73) (#111)
* Stop re-rendering the whole transcript on every streaming tick (v0.1.73) Long chat sessions crawled during streaming because each 150ms flush re-broadcast the full assistant message (with uncapped tool outputs) and the UI re-rendered — and re-parsed markdown/KaTeX for — every message in the session on every tick. Server: cap tool part output/error at 16k chars in TurnCtx::flush (one choke point covering all three harness adapters), with a visible truncation marker; the UI never rendered more than 20k chars anyway. UI: wrap Message, Md, ToolGroup and ToolRow in React.memo, and make the callback props (respond, openPlan, per-session file opens) referentially stable so the memoization holds. upsertMessage already preserves object identity for untouched messages, so only the actively streaming message re-renders now. * Review nits: drop inert memos, document the 16k/20k cap coupling ToolRow/ToolGroup get fresh part objects every streaming tick (the server re-broadcasts the whole message), so memo on them never skips — Message's memo is what short-circuits untouched messages. Keep memo only where it works (Message, Md). Also note on ToolRow's 20k slice that the backend caps at 16k so the slice must stay above it, and drop the render-site comment that moved to openFileInSession. |
||
|
|
54d0be7521 |
Live session-worktree viewer (Worktree tab) (#110)
* Add live session-worktree viewer ("Worktree" tab)
A new right-pane "Worktree" tab shows, for the active chat session, what the
agent is changing in its private git worktree before any run/commit exists:
- Changes view: status-badged changed-file list (complete even when the diff
truncates) plus the unified diff of the worktree vs merge-base(origin/<baseline>,
HEAD), untracked files rendered as new-file diffs.
- Files view: the full live worktree tree (shared CodeTab tree components),
clicks opening the existing FileViewer against the session's worktree.
Backend: new read-only GET /api/chat/sessions/{id}/worktree endpoint and a
sessionId param on the code-tree endpoint (threaded through resolve_checkout_root
instead of a hard-coded None; mutually exclusive with ref). New git.rs helpers
merge_base, working_tree_diff_against, and changed_files are all read-only.
UI polls every 5s only while the session is busy (chat.busy SSE), refreshes once
on busy->idle, manual refresh always; keeps last-good data on transient errors.
Entry point is a FolderGit2 button in the chat header when a session is active.
* Review fixes: clear worktree tab on project switch/session delete, seed busy poll on mount, changed-files dedup
* Worktree tab feedback: diff-only Changes view matching the experiment drawer, styled tooltip on the header button
* Tooltip: View session worktree
|
||
|
|
0329d50e3a |
Keep markdown table cells from breaking words mid-word (v0.1.72) (#109)
* Keep markdown table cells from breaking words mid-word .md's word-break: break-word shrinks each cell's min-content width to a single character, so auto table layout squeezed short columns (e.g. a "Round" header) into vertical letter stacks. Restore normal word-breaking in cells with overflow-wrap: break-word for genuinely overflowing tokens, and give .md tables the same scroll-in-place treatment as .report-md tables so whole-word column minimums can't clip in narrow panes. * Rebuild ui/dist * Bump version to 0.1.72 |
||
|
|
e1b6c9c608 |
Render LaTeX math in chat and report markdown via KaTeX (v0.1.71) (#108)
* Render LaTeX math in chat and report markdown via KaTeX Agents emit \(...\) / \[...\] LaTeX which plain markdown mangled (backslash escapes stripped, underscores italicized). Add remark-math + rehype-katex to the shared Md renderer and the Files-tab report renderer, with a fence-aware normalizer that rewrites backslash delimiters to the dollar forms remark-math understands. Applies to all harnesses since every transcript text part renders through Md. * Bump version to 0.1.71 |
||
|
|
3661374314 |
Composer command chips, optional paper args, drop /hf (v0.1.70) (#107)
* Composer command chips: picked slash-skills render as a removable unit - Picking a skill (menu, typing /name<space>, or the empty-state suggest button) lifts the command out of the textarea into a chip; the textarea holds only the args, with the skill's arg hint as its placeholder. - Backspace at position 0 (or clicking the chip) pops it back into editable /name args text; send() reassembles the plain wire form, so server-side expansion is unchanged. - User bubbles render a leading known /command with the same chip. - /reproduce-paper args are now optional: bare invocation tells the agent to infer the paper from the current repo and only ask if unidentifiable; arg hint is bracketed to signal optionality. - Drop the /hf skill. - Fix a latent IME bug: Enter during composition no longer sends. * Review nits: rename activeSkill to pickedSkill (avoids collision with the menu's activeSkillIdx), fix stale pending-question comment * Chip inline with the input line; paper-aware optional placeholder - The chip and textarea now share a flex row (.composer-input), so the command and its arg hint read as one line, Claude-desktop style. - When the project has an attached paper, the /reproduce-paper placeholder says the args are optional and names the paper it defaults to. * Chip overlays the textarea with first-line indent so args wrap full-width The flex-row layout squeezed wrapped args into a column beside the chip. Instead the chip now sits absolutely on the textarea's first line, the first line is text-indented past its measured width, and the chip rides the textarea's scroll (clipped at the wrapper) so scrolled text is never hidden behind it. * Claude-style arg hints and chip semantics - Bracket every skill's arg hint ([topic], [paper on compute], ...) to match the Claude-desktop placeholder convention. - The chip is inert like inline text: pointer-events none so clicks land in the textarea beneath, and Backspace at the start deletes the command outright (args stay) instead of dissolving it into /name text. * Hint reads [paper] on [compute] — bracket each supplied part, not the phrase * Paper-attached hint keeps compute; extend to /paper-to-marimo - Placeholder on paper-attached projects now reads '[paper — optional, defaults to <id>] on [compute]' for both paper-reproduction skills — only the paper part is optional; compute is still expected. - Both templates and /paper-to-marimo's no_args now tell the agent to infer the paper from the repo when the args name none, so a compute-only invocation works too. * v0.1.70 |
||
|
|
a7cc9947a7 |
Codex: render every item type in the transcript; orx-lit-first routing rule (#105)
* Render every Codex item type in the transcript; route orx-lit first
The Codex chat adapter's apply_item whitelisted five ThreadItem variants
(agentMessage, reasoning, commandExecution, fileChange, plan) and dropped
everything else through a `_ => {}` catch-all. Codex reports its web
searches, MCP tool calls, and dynamic tool calls over the wire, so those —
and any future protocol addition — vanished from the transcript.
Replace the catch-all with explicit arms for webSearch, mcpToolCall, and
dynamicToolCall (rendered as tool parts, parity with the Claude harness), an
explicit skip for userMessage/hookPrompt (input echoes that would duplicate
the user bubble), and a generic fallback that renders any other item type as
a tool part named after its raw type — nothing is silently swallowed. The UI
toolLine now shows the WebSearch query and the WebFetch url. Rebuild ui/dist.
Also rewrite the orx-lit skill description as a routing rule: use it FIRST,
before any web search, for anything academic or research-flavored (paper,
author, blog post, model release). GPT skipped orx-lit on research queries
because the old description read as experiment-loop-only. Mirrored across the
code const, the skill frontmatter + body, SYSTEM_PROMPT.md, and root SKILL.md.
* Render codex web-tool actions (openPage/findInPage), not just search queries
The webSearch item's query is legitimately empty for openPage/findInPage
actions; the action object carries the url/pattern. Merge action fields
into the tool part input and label each variant in the UI.
* Label opaque codex web-tool actions as Browsed the web
codex 0.144 reports page visits as action {type:"other"} with no url —
the openPage/findInPage variants exist in the protocol but aren't emitted
by the hosted web tool yet. Stop calling those rows searches.
* orx-lit description: preferred literature tool, not a web-search sequencing rule
The 'use FIRST, then web-search what the corpus lacks' phrasing implied a
web search always follows. Intent: orx-lit is the preferred method for
literature search on academic/research work; web search is a fallback and
often unnecessary.
|
||
|
|
7cfef22bf6 |
Tour copy and sizing polish (v0.1.69) (#104)
* Tour copy and sizing polish Rewritten step descriptions (workspace pitch, prompt examples, worktree framing) with no em dashes and a single colon across the deck. Card sized between the original and the first enlargement: 390px wide, fs-lg title, fs-base body, full-size footer buttons. * Bump version to 0.1.69 |
||
|
|
2dd67bc2ca |
UI design-consistency pass: token scales, sentence case, sans-first, rounded everywhere (v0.1.68) (#103)
* ui: add type/weight/radius token scales, drop dead CSS rules * ui: drop all uppercase transforms and tracking, sentence-case status strings * ui: reserve mono for code, paths, ids and terminal output * ui: primary content uses --text; grays reserved for true secondary text * ui: normalize font weights onto the --fw-* scale * ui: normalize radii and type sizes onto token scales, round callouts and inputs * ui: pin badges to sans in mono contexts, un-mono instance durations; rebuild dist * ui: sentence-case remaining status strings, fix stale ModelPicker comment * ui: promote composer reasoning pill to --text * ui: shared Wordmark component — logo mark + all-black OpenResearch text * ui: match home wordmark weight to the landing page (800 -> 700) * chore: bump version to 0.1.68 |
||
|
|
0a42fb8b28 |
Onboarding tour: spotlight walkthrough of the workspace (#102)
* Onboarding tour: spotlight walkthrough of the workspace Seven-step tour (welcome, composer, model picker, Files, Compute, experiments panel, new session) shown once after first-run setup and replayable from a help button in the chat header. Ported from the openresearch.sh onboarding system: data-onboarding attribute targeting, ResizeObserver/MutationObserver bounds tracking, box-shadow spotlight dim, viewport-clamped card with pointer arrow. Completion persists to localStorage (orx:tour-done). Escape is captured so it can't interrupt a running agent turn. * Tour polish: exact-primary ring, data-tip tooltip on replay button The ring's 45%-alpha glow plus a 1.5px border read as a lighter red than the theme primary; a crisp 2px border with no glow matches it exactly. The header help button now uses the app's data-tip tooltip instead of a native title. |
||
|
|
aba90b6298 |
Files tab redesign: split explorer with inline preview (#100)
Replace the pill-header list + full-page report swap with a master-detail split: a resizable file tree on the left (folders open by default, report folders titled by their report, dir path demoted to a copyable footer) and a persistent preview pane that renders everything inline — reports and markdown as GFM docs with a view-source toggle, images and PDFs directly, other text as highlighted code via the new shared CodeView (also adopted by FileViewer), with an Open-raw fallback for binary/oversize files. Icon-only controls get a CSS data-tip tooltip primitive; empty states for no-files and nothing-selected; FilesTab keyed by project and PreviewPane keyed by path so state can't leak across switches. |
||
|
|
a72c4cdc2b |
feat(codex): native plan mode as a selectable permission mode (v0.1.66) (#98)
* feat(codex): native plan mode as a selectable permission mode
Codex ≥ 0.144 supports plan mode as an app-server collaboration mode.
Advertise Plan + Auto + Bypass for codex and wire the full flow:
- initialize sends capabilities.experimentalApi (unlocks collaborationMode)
- conditional per-turn collaborationMode mask: plan on plan turns; default
on a non-plan turn only when the thread may be sticky-planned (DB
prev_permission_mode threaded via TurnCtx + in-memory last-sent tracking);
otherwise no mask (a mask always injects a template)
- render streamed plan items (item/plan/delta + completed plan item) and
synthesize an end-turn plan card / PlanStrip
- requestUserInput -> question card, answerable inline or via composer;
per-kind server-request settle shapes (approval/userInput/other)
- resume_from_prompt: plan (send-message), permission/question (inline reply)
- store: keep 'plan' off the codex retire-list; reuse claude.rs
synthesize_resume / should_synthesize_plan via harness re-export
- UI: PlanStrip agentLabel; ChatPanel pendingQuestion gate includes codex
Legacy exec path (< 0.144) keeps Plan->read-only, with an updated agent note.
* fix(chat): keep prompt-resume scaffolding out of the transcript
Plan/permission card resumes send synthesized text the user never typed
("Implement the plan.", "The user approved that action. Continue.").
Persisting it as a user message misattributes it in the UI. Split the
transcript text from the harness text in send_message (the slash-skills
precedent): plan/permission resumes now record only the user's own note
— nothing at all when there isn't one; the resolved card tells the rest.
Question resumes still record the answer, which the user did choose.
* chore: bump version to 0.1.66
|
||
|
|
66c8159511 |
Match the web app's favicon and retitle the tab to OpenResearch CLI (#96)
Ports favicon.svg from openresearch.sh so the embedded UI's browser tab shows the same brand mark, and retitles the tab "OpenResearch CLI" to distinguish it from the hosted web app. favicon.svg joins index.html in the no-cache branch since it's served under a fixed (unhashed) name. |
||
|
|
67190285ab | chore: support automated reproductions (#95) | ||
|
|
87df17ce0f |
chore: Report md heading styles (#91)
* Report pane: restore Markdown heading hierarchy in report-md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bump version to 0.1.63 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
012672958d |
Plan-mode chat: AskUserQuestion double-card/hang fix + desktop-parity plan/question cards (v0.1.61) (#84)
* Fix harness prompt bugs: AskUserQuestion double-card + hang, resolved-card rendering
- plan_auto_policy auto-allows AskUserQuestion: the bridge no longer fronts
the question card with a redundant permission card whose held request also
deadlocked the end-turn resume (answer rejected as busy until interrupt).
- respond() resolves a card only after its answer is delivered, so a rejected
send leaves the card actionable instead of resolved-with-the-answer-dropped.
- Resolved cards mirror Claude Code: permission cards vanish, plan/question
cards collapse to compact inline expandable rows with an answer echo
(answers/approved/note stamped on resolve, carried through turn flushes).
* Review round 1 fixes: doc split, echo-preserving adopt, neutral outcome, read-only class rename
* Review round 2 fixes: PendingGuard doc, best-effort resolve folded into helper, neutral question echo
* Post-review nits: doc wording, dedupe question text in collapsed body, word-break on collapsed rows
* Plan-mode AskUserQuestion truly blocks: bridge holds the call as the question card
Auto-allowing AskUserQuestion was wrong: headless the tool returns no
answer, so the model guessed and kept planning instead of waiting. The
bridge now surfaces the QUESTION itself as a held mid-turn card (like
ExitPlanMode's plan card); the answer settles the held call as a denial
whose message is the answer, so the model continues the same turn with
the user's actual choice. The turn-status line says 'Waiting for your
input…' instead of 'Working…' while a card holds the turn open.
* Post-review nits: deny-message hardening, question cards don't suppress plan-synth, empty-options fallback, held-card status via nativeId, comment accuracy
* Polish: no tool row for bridge-held calls (deny-with-answer painted them red), plan pane gets padding + plain background
The bridged AskUserQuestion/ExitPlanMode tool_use now renders nothing —
the held card is the representation, and the denial that carries the
user's answer back made the CLI report the tool as errored, painting a
successful Q&A as a red error row. The right-pane plan tab had no CSS
at all: flush text on the panel tint; it's a markdown document, so it
gets a padded, plain reading surface.
* PlanStrip: Claude-desktop shape — title line, real buttons in a row below
The strip's buttons used .btn-primary/.btn-ghost, which are only styled
inside .prompt-actions — in the strip they rendered as bare text crammed
next to the title. The actions row now reuses .prompt-actions (same
buttons as every card) and sits on its own line under the title.
* PlanStrip buttons: monochrome — black text/borders, no brand red
* PlanStrip split button: single internal divider (caret drops its own left border)
* PlanStrip split button: flat inner corners (radius reset must out-rank .prompt-actions shorthand)
* Composer: attention bell instead of Stop while a held card awaits the user
A blocked-on-input turn is busy, so the composer showed the Stop X — a
loading/cancel affordance while the agent is actually waiting on an
answer. When the session is busy AND a held card (nativeId) is
unresolved, show a pulsing bell that scrolls to the pending card; Esc
still interrupts. The transcript status line shares the same
awaitingInput signal.
* Composer: two states only — Send by default, Stop X only while actively streaming
Drop the bell experiment. A turn held on an unanswered card shows the
idle Send button; the card and the status line are the waiting
affordance. Esc still interrupts a held turn.
* Composer answers the pending card (Claude-desktop): typed text = plan revision feedback / custom question answer
- send() routes plain typed text to the newest unresolved plan/question
card (respond with note) instead of racing the held turn with a new
message the busy guard would drop; placeholder says so. Questions
route only on Claude sessions (opencode rejects note-only replies).
- Rail busy-dot stops pulsing when the turn is held waiting on the user
(steady dot; same awaitingInput signal as the status line).
- Note-only question answers echo the note in the collapsed row.
* PlanStrip: Claude-desktop parity actions + popovers above the strip
Reject / Revise… / Accept / Accept-and-auto-mode (+ bypass in the caret),
Open plan as a title-row link. Reject is a true rejection: the note-less
plan denial now tells the model to stop and wait (was 'keep refining',
which made it improvise revisions/questions); collapsed rows echo
'Rejected' vs 'Revision requested' by note presence. Accept resumes
under accept-edits (guarded), Accept-and-auto under Auto — the primary
is the solid dark button, not brand red. The strip moved inside
.composer so the mode/model picker popovers (z 50 in that stacking
context) layer above it — as a sibling, .composer's z-index:4 capped
them below the strip.
* PlanStrip: single Accept-and-auto-mode primary, drop the plan excerpt
Matches the desktop reference exactly: [Reject] [Revise…] on the left,
one primary button on the right whose caret menu holds the two guarded
tiers (Accept = accept-edits, Accept and bypass all). The separate
outline Accept button and the plan-title excerpt next to the header
are gone — plan.excerpt/plan prop was fully unused after removal.
* PlanStrip: self-contained Revise mode (Claude-desktop parity)
Swaps the strip into its own textarea + [Back] [Revise ↵] footer
instead of routing through the main composer — matches the desktop
reference exactly. Enter submits, Shift+Enter newlines, Esc cancels
back to the action row. The main-composer typed-text routing
(pendingPrompt) is now question-only; plans have their own dedicated
UI and no longer need it.
* PlanStrip: drop the plain accept-edits Accept option
We have no partial-approval story (edits-only, everything else still
gated) — the caret now holds only Accept and bypass all.
* Plan revision: visible feedback + never silently dead-end
- Submitting a revision now swaps the strip to a 'Revising the plan…'
placeholder until the revised card arrives (or the turn ends) —
previously the strip snapped back to an identical-looking action row
and the submit read as a no-op.
- An empty revise note maps to a generic 'Please revise the plan.'
instead of riding the note-less wire shape, which the backend reads
as a hard rejection ('stop and wait') — the '(optional)' field was
accidentally a Reject.
- respond() reconciles messages AND busy state from the server after
every answer: a stale card (e.g. resolved by the bridge timeout while
the tab's SSE was dropped) previously stayed actionable forever and
swallowed every answer.
* PlanStrip: drop the 'Revising the plan…' placeholder
The transcript's Working… spinner already communicates it. The strip
still hides while a submitted revision is in flight (the outgoing card
must not look actionable) and the revised card swaps in on arrival.
* Review round 1 fixes: zombie-card recovery, honest composer button, reject-loop fix
Blockers:
- A held card orphaned by a process restart (PendingGuard never ran)
permanently captured typed input: the composer routed every message to
it and errors were swallowed. Now the backend collapses the zombie on
the first answer attempt, and the composer only routes typed text to a
question card that is actually answerable (live turn for native cards).
- The Send/Stop swap keyed on awaitingInput while send() could only
service question cards — a held plan/permission card showed a lying
Send (silent no-op) with no Stop control. Now keyed on pendingQuestion.
Also: note-less Reject on an end-turn/synthesized plan card resolves
with no resume (resuming just to say 'stop' re-synthesized a card — an
un-dismissable strip); the bridge plan-deny arm reuses synthesize_resume
verbatim; respond() reconciles busy for its own session only and reports
failure so the question route can restore a consumed draft; revising
clear gated to its session; un-docked plan card fallback matches the
strip's semantics; Rejected gets its own outcome class; note-only
answers echo once; split-button CSS consolidated; dead CSS/union
removed; test pins the reject wording.
* Post-clean-round nits: keep optimistic busy on failed reconcile fetch, sweep zombie held cards at claude turn entry
- respond()'s busy reconcile no longer clears busy when the sessions
fetch fails — during a Handled resume nothing re-asserts busy
mid-stream, so a transient fetch failure would hide Working…/Stop for
the rest of the turn.
- claude run_turn sweeps unresolved native_id cards at turn entry
(resolve_stale_prompts gains a native_only mode; codex keeps sweeping
everything). A held card can never outlive its process, so one left
unresolved is a crash artifact — previously it could capture the
composer's typed-text routing once the new turn made the session busy.
* Bump version to 0.1.61
|
||
|
|
7450249cec |
Compute tab redesign: unified target list + default compute target (#86)
* Settings: persist default compute target (backend + flavor) with pure resolver * Launch paths: resolve the default compute target; widen HTTP run to all 7 backends * API: compute-targets summary, default-target POST, local hardware + openresearch status endpoints * Agent guidance: reveal the default compute target in the playbook, SKILL.md, and skill templates * Compute tab: unified target accordion with This-machine + OpenResearch rows and default picker * cargo fmt * Polish: summary casing, a11y-clean row controls, instances subtitle * Review round 1: coherent no-flavor default prompt, 500 on settings io errors, UI dedup + summary refresh, doc fixes * Clean-round nits: refetch race guard, aligned token-source labels, flavor-required const, doc accuracy * Quiet the agent-disclosure footnote: muted text instead of amber callout * Info icon on the agent-disclosure footnote |
||
|
|
866ccd4ec0 |
Session code browser + GitHub branch links (#85)
* Session code browser + GitHub branch links
Backend: GET /api/projects/{id}/code-tree?sessionId= lists the chat
session's worktree via git ls-files (branch name included); factor the
worktree/clone root resolution shared with project_file into
resolve_checkout_root. Add current_branch + list_worktree_files git helpers.
UI: per-session Code right-panel tab (CodeTab) with a collapsible file
tree that opens files in the existing FileViewer; a Code button in the
chat header; GitHub tree/<branch> links (per-segment URL-encoded) on
experiment cards (TreeView), the Changes header (DetailDrawer), and the
Code tab header. Rebuild committed ui/dist.
* Code tab: browse experiment branches, not just the live worktree
Sessions usually sit detached on the baseline tip, so the worktree-only
view showed baseline code instead of the experiment's. The tab now has a
source select: live session worktree or any experiment branch's committed
tree (default: the branch of the latest run's experiment).
Backend: code-tree and file endpoints take an optional ref= param —
resolve_branch_commit (local ref, then origin; option-injection guarded)
+ list_tree_files (ls-tree) / file_at (git show); root:"branch" in
responses; 404 when the branch doesn't exist. Live mode unchanged.
UI: getCodeTree/getProjectFile take {sessionId?, ref?}; FileViewDef
carries the ref through FileViewer (branch badge, ref-aware not-found
copy); polling only in live mode; GitHub link follows the selected ref.
Rebuild committed ui/dist.
* Code tab review round 2: lift tab state, stream branch-file reads
- CodeTab source + expansion state move onto the App-side tab def
(CodeTabDef.sel/selPicked/toggled): the component unmounts whenever a
file tab fronts it, and remount must not rederive the default against
newer runs or lose expansion. Default computed once in openCodeTab.
- file_at_capped: cat-file -e existence check (exit code, no stderr
parsing) + streamed cat-file blob read capped at limit+1 bytes — a
committed multi-GB blob no longer means a full-blob allocation, and
content is byte-exact (no trim). Used by project_file's ref arm;
file_at kept for its small-file callers.
- resolve_branch_commit rejects rev-suffix expressions (@{, ^, ~, :,
whitespace); doc comments cross-reference branch_head_sha's opposite
local/remote preference.
- CodeTab load lifecycle: request-id guard covers stale sources,
superseded polls and post-unmount responses; manual Refresh surfaces
errors (inline note when a tree is showing); a seeded default branch
that doesn't exist yet falls back to live once, user picks keep the
error. split_nul helper dedups the NUL-split decode. Copy/comment
alignment. Rebuild committed ui/dist.
* Code tab post-review polish
- file_at_capped: reap the child before propagating read errors (no
zombies); after a complete read, a failing cat-file exit (tree path
via crafted request, vanished object) is an error, not a fake empty
file. Kill only on read error/truncation — after EOF git exits on its
own, and killing then could race a bogus signal-death status.
- list_worktree_files drops dir entries ('nested/' from a nested git
repo under --others) server-side.
- openCodeTab's updater self-guards against duplicate defs.
- Seeded default branch selection falls back to live on any fetch
failure (best-effort), not just 'branch not found'; user picks keep
errors. Doc/copy alignment; drop unreferenced tree row classNames.
Rebuild committed ui/dist.
* Tree cards + file viewer UX: Code shortcut, labels, line numbers
- Cards: labeled Changes/Logs/Code actions (Terminal renamed), GitHub-mark
icon link at the card edge; width 240->264 so the row fits; no phantom
selection outline or pointer cursor (selection state was write-only and
is removed; RF nodes stay selectable so pointer-events keep the buttons
alive, visuals suppressed in CSS)
- Card Code button opens the project-level Code tab pinned to that
experiment's branch (CodeTabDef.sessionId nullable); chat-header Code
button removed along with the session-tab opener it fed
- Changes/Code headers: BRANCH/COMMIT/SOURCE micro-labels; shared GitHubMark
(lucide has no brand icons) used by pill, cards, and code-tab link
- File viewer: sticky line-number gutter (1:1 with unwrapped rows) and a
slimmer header
- ui/dist rebuilt
* Code tab: collapse the dead session path
The chat-header opener was removed deliberately (
|
||
|
|
874e84080c |
Plan UI: docked approval strip + right-pane plan tab (#83)
* Plan gate: read-only git verbs + pipelines into pure consumers + 2>&1
Planning agents read experiment code that lives only on git branches
(git show <ref>:<file>, git ls-tree) and habitually pipe output through
head/grep with a stderr-merge — all of which the gate rejected outright
via its whole-command metachar blacklist. Rework classification to be
per-pipeline-stage:
- segments (;, &&) split into pipe stages; stage 0 must be a read-only
producer (orx read, git read, glue, or pure consumer), later stages
pure consumers (head/tail/grep/wc/cat/sort/uniq/cut/tr/nl/column —
tee/xargs/sed/awk deliberately excluded)
- standalone 2>&1 tokens dropped; any other token carrying > or & (or
backticks/$(/</newlines anywhere) still gates the whole line
- read-only git allowlist: whole-verb reads (status/log/show/diff/grep/
ls-tree/...) with --output and -O/--open-files-in-pager escape-hatch
guards; conditional forms for branch/tag (list-query only), stash
(list/show), remote (bare/show/get-url), worktree (list), reflog;
global flags limited to -P/--no-pager/-C (rejecting -c config
injection); git config excluded wholesale
- command_is_readonly exported for reuse by the upcoming MCP permission
bridge auto-policy
Fail-safe allowlist property preserved: unknown programs, verbs, or
unparseable forms gate the entire command.
* Plan mode: synthesize a plan card when ExitPlanMode never fires + accept-edits option
Observed failure: a plan-mode session ended with the plan as plain chat
text and no ExitPlanMode call — and since only a plan-card answer
switches the resume mode, the user's chat 'yes' resumed still in plan
mode, stranding the session read-only. Root cause of the missing call:
the shared playbook still said 'Never call an interactive
question/elicitation tool — it will hang', guidance that predates the
prompt-card work in #37/#40.
- run_turn tracks (plan_mode, saw_prompt, turn_errored, last_text) and
synthesizes a WirePrompt{kind:plan, synthesized:true} from the final
text when a plan-mode turn ends card-less and error-free; the pure
condition fn is unit-tested
- WirePrompt/ChatPrompt gain ; the UI softens the header to
'Plan mode — ready to proceed?' for synthesized cards
- playbook: interactive tools surface as cards now; in plan mode, always
present the finished plan via ExitPlanMode
- plan card gains 'Approve & accept edits' (resumeMode accept-edits —
synthesize_resume already honors it)
* chore: drop stray npm lockfile (ui uses pnpm)
* Plan mode: MCP permission bridge — desktop-style mid-turn approvals
Headless plan mode had no approval loop: gray-area tools hard-denied
with nobody to ask, WebFetch was denied (verified on claude 2.1.197),
and — worst — ExitPlanMode SELF-APPROVED headless ("User has approved
exiting plan mode", nobody asked), letting the model exit plan mode and
edit files without the user's say.
The fix is a permission bridge on Claude Code's (hidden but functional)
--permission-prompt-tool flag:
claude ── --mcp-config ──▶ orx mcp-gate (stdio MCP server, hidden cmd)
│ HTTP long-poll, 127.0.0.1:<up port>
▼
orx up: POST /api/internal/permissions ─ oneshot ─▶ card (SSE)
user answers /respond ─▶ ResumeAction::Handled ─▶ claude unblocks
- src/commands/mcp_gate.rs: newline JSON-RPC stdio server (initialize /
tools/list / tools/call), concurrent calls, single stdout writer; any
transport failure answers deny — never hangs claude
- ChatHost: request_permission (token gate -> tier-1 auto-policy ->
card + held oneshot, 55-min timeout under MCP_TOOL_TIMEOUT=60min),
settle_permission, per-session drain on turn end/interrupt, drop-guard
so an abandoned request always resolves its card
- auto-policy (pure fn, tested): read-only Bash allow (shared
plan_gate classifier), WebFetch/WebSearch allow, file edits DENY —
with a permission tool configured the CLI delegates plan mode's edit
block to it (verified: an allow creates files mid-plan), so that deny
IS the safety
- plan_gate hook now also matches ExitPlanMode and answers "ask",
routing plan approval to the bridge card instead of self-approval;
without a bridge, ask denies headless — still better than self-approve
- claude.rs: write_mcp_config + flags wired per plan-mode turn
(best-effort; older CLIs silently ignore the flag and degrade to
today's gating), ExitPlanMode tool_use renders as a plain tool row
while the bridge card is authoritative, bridge cards count as
"saw a prompt" for the synthesized-card fallback
- resume_from_prompt: native (mid-turn) branch — permission allow/deny
and plan keep-planning settle the held request in place (Handled);
plan approve interrupts the paused plan turn and resumes via the
proven --resume + mode-flip machinery (respond()'s SendMessage arm
now interrupts first for native cards only)
Empirical grounding (claude 2.1.197 spikes): denials don't fail headless
runs (permission_denials + subtype success); WebFetch denied natively in
plan mode; hook allow bypasses the permission tool (no card spam);
bridge tool timeout = tool error, turn continues; hook "ask" on
ExitPlanMode routes it through the bridge and an allow resumes the same
turn.
* Plan UI: docked approval strip + right-pane plan tab
The plan card scrolls away with the transcript and renders the whole
plan inline. Desktop-style flow instead:
- PlanStrip (new): docked between the thread and the composer while a
plan awaits a decision — "View plan" / "Keep planning" / "Approve &
run" with a split-menu for accept-edits / bypass. Keep-planning
consumes the composer draft as the refinement note, so typing
feedback then clicking the button is the natural gesture. Solid card
floated over the composer's gradient fade; disappears on resolve via
the SSE re-emit.
- Plan tab: "View plan" opens the markdown as a right-pane ClosableTab
(PlanViewDef in the rightTab union, one tab per prompt id, no fetch —
the text is already client-side); file links inside the plan resolve
against the plan's session worktree.
- The inline plan card goes compact while a strip host exists: clamped
to ~9.5em with a fade, "View full plan" link, no buttons (the strip
owns the actions; single source of truth). Without a strip host the
card keeps its full button row.
* Store: stop normalizing claude-code plan sessions to auto
The retired-modes migration runs on every Store::open and still listed
'plan' for claude-code — written when plan mode was dropped (pre-#75).
Re-adding plan to options() without updating it meant picking Plan
silently degraded to Auto on the next request; the composer masked it by
re-sending the mode with every message, but any turn without an override
(API callers, resumes) ran in auto. Caught live in the plan-mode E2E.
* chore: bump version to 0.1.60
|
||
|
|
7d1aed6f7d |
Plan mode: synthesized plan card fallback + accept-edits option (#81)
* Plan mode: synthesize a plan card when ExitPlanMode never fires + accept-edits option
Observed failure: a plan-mode session ended with the plan as plain chat
text and no ExitPlanMode call — and since only a plan-card answer
switches the resume mode, the user's chat 'yes' resumed still in plan
mode, stranding the session read-only. Root cause of the missing call:
the shared playbook still said 'Never call an interactive
question/elicitation tool — it will hang', guidance that predates the
prompt-card work in #37/#40.
- run_turn tracks (plan_mode, saw_prompt, turn_errored, last_text) and
synthesizes a WirePrompt{kind:plan, synthesized:true} from the final
text when a plan-mode turn ends card-less and error-free; the pure
condition fn is unit-tested
- WirePrompt/ChatPrompt gain ; the UI softens the header to
'Plan mode — ready to proceed?' for synthesized cards
- playbook: interactive tools surface as cards now; in plan mode, always
present the finished plan via ExitPlanMode
- plan card gains 'Approve & accept edits' (resumeMode accept-edits —
synthesize_resume already honors it)
* chore: drop stray npm lockfile (ui uses pnpm)
|
||
|
|
203f63b900 |
Configurable data directory: choose, change, and move with progress (#80)
Let users pick where orx stores everything (orx.db + WAL, run-logs/, artifacts/, chat-attachments/, agent-*.log) instead of assuming ~/.local/share/openresearch. The default is unchanged; the location is now choosable from the product. Resolution order in store::data_dir(): $ORX_DATA_DIR (forced override) → persisted user choice → $XDG_DATA_HOME → default. An explicit UI choice beats the ambient XDG base; the env var stays authoritative for sandboxes/tests. Empty env vars are treated as unset. - Persisted path lives in config_dir()/settings.json, OWNED by telemetry's locked+atomic mutate_settings (in-process mutex + cross-process flock + temp -and-rename + corrupt-file refusal). data_dir is a field on telemetry::Settings so a write can't clobber installId/telemetryDisabled and vice-versa. - Move engine (src/local/datadir.rs): validate → WAL checkpoint → same-fs rename (atomic, consumes old) or cross-fs copy-with-progress (verify, then KEEP the old dir, reported as oldPathLeft — never deletes data a stray writer might still be using) → swap the persisted path. Free-space pre-check via statvfs; EXDEV falls back to copy. - Endpoints (up.rs): GET/POST /api/settings/data-dir, /validate, /move. Move streams datadir.move.progress/done/error over the existing SSE broadcast and returns 202. A move-in-progress flag (claimed via compare_exchange) makes every store-mutating handler back off with 409 for the duration. - After a move, harness children are restarted so any that pinned the old $ORX_DATA_DIR (Codex) respawn on the new path. UI: - Settings → Storage card: current path + source, validate, move with a new byte-level progress bar, env-forced read-only state, in-flight 409 handling. - Onboarding gains a "Storage" step (default pre-filled, skippable). - Files view links "Change storage location →" to Settings → Storage. - Chat send surfaces the move-in-progress 409 and restores the draft instead of dropping it. human_bytes shared via store::human_bytes; fmtBytes shared via api.ts. |
||
|
|
08d30a8e61 |
Add anonymous, opt-out usage telemetry (PostHog) (#70)
* Add anonymous, opt-out usage telemetry (PostHog)
`orx` shipped with no telemetry, so there was no way to measure installs,
DAU/WAU, retention, or command usage. This adds anonymous, opt-out analytics
sent to a dedicated PostHog project.
What's collected: a per-invocation `cli_command` event and explicit
`experiment_started` events (create + run, with a coarse compute target),
carrying only a random per-install UUID, CLI version, os/arch, and CI flag.
Never any code, prompts, file contents, ids, names, paths, or tokens.
Design:
- New `src/telemetry.rs`: settings (install id + opt-out) at
`config_dir()/settings.json`, opt-out policy, event send, and a
fire-and-forget session modeled on `updates::UpdateWarning`.
- Opt-out: `DO_NOT_TRACK`, `ORX_TELEMETRY=off`, `CI`, a `--no-telemetry`
global flag, and a persistent `orx telemetry on|off|status` subcommand.
A one-time disclosure prints after the first `orx login` (shown even when
disabled, so it's never permanently suppressed).
- Never blocks or crashes the CLI: events are spawned and flushed once at
exit within a bounded window, so success output is never delayed; all
errors are swallowed.
- Cross-process safe: settings writes use an `fd_lock` advisory lock plus
atomic temp+rename, so a concurrent write can't revert a persisted opt-out.
- musl-safe: reuses the rustls `reqwest` client; no new TLS/C dependency.
Events land at PostHog `/i/v0/e/` with a public write-only `phc_` key and a
client-side ISO-8601 timestamp. `$process_person_profile:false` keeps events
anonymous. Unit tests cover opt-out precedence, id stability, payload shape
(PII-free), timestamp math, corrupt-file fail-safe, and the non-blocking
flush path.
* Prefix all CLI events with `cli_` to separate from website analytics
This PostHog project is shared with the website/cloud-agent analytics, so CLI
events must be distinguishable by event name alone — relying only on the
`source: "cli"` property is fragile (one forgotten dashboard filter leaks CLI
events into website metrics).
`build_payload` now stamps a `cli_` prefix onto every event name centrally, so
every current and future CLI event is `cli_*` by construction. Wire names:
- `cli_command` (was `cli_command`)
- `cli_experiment_started` (was `experiment_started`)
Added `every_event_name_is_cli_prefixed` to lock the guarantee.
* Telemetry: trim opt-out surface to --no-telemetry + `orx telemetry off`
Drop the `DO_NOT_TRACK`, `ORX_TELEMETRY`, and `CI` environment-variable
opt-out/disable checks. They were redundant with the two mechanisms we keep:
the `--no-telemetry` per-run flag and the persistent `orx telemetry off`.
The `CI` auto-disable in particular guarded a scenario that's mostly
hypothetical for an interactive research CLI, and it only ever fired for
hosted CI providers that set `CI` (missing Jenkins, agent boxes, and ad-hoc
scripts — the automation `orx` actually runs under). Every event still carries
a `ci` property, so automated runs can be filtered at query time in analytics
without being dropped up front.
Updates the opt-out policy, first-login notice text, `--no-telemetry` help,
README, and tests accordingly.
* orx up: add usage-analytics step to onboarding
Adds a third onboarding step ("Usage analytics") to the `orx up` dashboard
first-run walkthrough, after the coding-agents and git/GitHub steps. It shows
what is and isn't collected and lets the user toggle anonymous analytics On/Off
before creating their first project. On by default (matches the CLI's opt-out
model); the choice persists via the same `telemetry_disabled` setting as
`orx telemetry off`.
- Backend: `GET/POST /api/settings/telemetry` in up.rs, reusing
telemetry::disabled_reason / set_persisted_disabled (mirrors the git-settings
handler pattern). Returns `{ enabled, reason }`.
- UI: getTelemetry/setTelemetry in api.ts; Onboarding.tsx grows to a 3-step
flow with a TelemetryCard (On/Off segmented control).
Verified end-to-end in orx up: toggling Off in the wizard persists
`telemetryDisabled: true` to settings.json; toggling On clears it.
* Telemetry: drop the CLI login stderr disclosure
The one-time `orx login` stderr notice (show_notice_once) is removed now that
the disclosure lives in the `orx up` onboarding step, which is the right place
for it. Also removes the now-unused `notice_shown` settings field and updates
the module doc + README to point at the onboarding step instead of a login
notice.
* Telemetry: record consent decisions (cli_telemetry_consent)
Adds one event, `cli_telemetry_consent { agreed: bool }`, that records whether
the user opts into or out of analytics — so we can count agrees vs rejects.
Unlike every other telemetry event, this one is UNCONDITIONAL: it bypasses the
opt-out check and the persisted install id, and uses a fresh anonymous
distinct_id per call. If it honored the opt-out, every rejection would be
invisible and the ratio would be meaningless.
Fired once when the user leaves the analytics choice:
- `orx up` onboarding: on "Create your first project" (records the final On/Off
state, so users who accept the default On are counted too) via a dedicated
POST /api/settings/telemetry/consent endpoint.
- CLI: on `orx telemetry on` / `orx telemetry off`.
Tests: consent payload shape + name prefix, and that record_consent sends even
when persisted-off without generating an install id.
* Bump version to 0.1.57
Ships the anonymous opt-out telemetry subsystem, the orx up onboarding
usage-analytics step, and the cli_telemetry_consent event. Merging this PR
cuts release v0.1.57.
|
||
|
|
36b7058f98 |
Chat: Escape stops the streaming turn (all harnesses) (#69)
Press Escape to interrupt an in-flight chat turn and drop focus back into the composer, mirroring the Claude Code desktop app. Works for every harness — a chat-view-scoped, busy-only document keydown listener calls the existing stop() -> interruptChat, which the backend routes to the active session's harness (Claude, Codex, OpenCode, ...). Also hardens the shared usePopover() hook to own Escape while open (capture phase + stopPropagation) — a pre-existing gap the new listener surfaced: without it, closing a composer picker with Escape would also interrupt the turn. The slash menu keeps its bubble-phase preventDefault path, gated by the listener's defaultPrevented check. Rebuilt ui/dist (tracked; debug reads from disk, release embeds it). |
||
|
|
bd9f438501 |
Chat: rename sessions from the Recents kebab menu (#68)
Add a "Rename" item to the per-row three-dot menu in the chat Recents
list (alongside Archive/Delete). Selecting it turns the row title into an
inline input — Enter/blur saves, Esc cancels — with an optimistic update
that reconciles with the server's chat.session broadcast.
Backend: extend the existing PATCH /api/chat/sessions/{id} handler to
accept a `title` field (trimmed, empty rejected) and add ChatHost::set_title,
mirroring set_archived (re-read after write + SSE emit).
Also guard harness auto-titling: TurnCtx::set_title (opencode's
session.updated path) now sets the title only when the session has none yet,
via an atomic conditional UPDATE, so it can't clobber a title the user set
via Rename.
Incidental: make the vite dev proxy target overridable with ORX_BACKEND
(defaults to the standard 4791) for testing against a non-default backend.
|
||
|
|
2c08a60fed |
Add OpenResearch compute as a backend: --backend openresearch (#67)
* Add OpenResearch compute as a backend: --backend openresearch
An ephemeral org-billed platform box per run, symmetric with the other
external backends. Submit validates flags, resolves the org, preflights the
user's registered SSH keys, pushes the branch, provisions via POST /sandboxes
(402 -> top-up message), records the run as starting, and detaches the
supervisor. The supervisor polls GET /sandboxes/{id} until the box is online,
records the SSH endpoint on the descriptor (restart-safe), launches the
clone-and-run payload over ssh wrapped in a wall-clock timeout guard, runs
the shared ssh watch loop, and deletes the box on every exit path — logs are
drained off the box before teardown, and a failed teardown appends an
'orx instance delete' hint to the run instead of changing its status.
Supporting changes:
- jobs/ssh.rs: SshTarget {dest, extra_opts} so the transport reaches
explicit user@host:port endpoints (openresearch) as well as config
aliases (ssh/slurm); ControlPath hash folds in the opts.
- supervise.rs: run_ssh's two-half loop extracted into watch_ssh_job,
shared by ssh and openresearch.
- Descriptor: sshHost/sshPort/sshUser + timeoutSecs optional fields
(fixed field list — unknown keys do not survive a round-trip).
- store: set_backend_json (narrow setter; upsert would clobber
status/markdown and race the cancel flag).
- client: get_sandbox / delete_sandbox / list_sandboxes / list_ssh_keys.
- orx instance list/delete: the manual cleanup path for a leaked box.
- UI: OpenResearch badge in BackendLogos; startRun type widened.
Requires the platform's companion PR (GET /sandboxes/{id} + PAT auth on
sandbox list/delete and ssh-keys list); against an older API the ssh-key
preflight degrades to a warning and provisioning-wait fails the run with
the API error after teardown retries.
* Bump version to 0.1.56 — releases --backend openresearch on merge
|
||
|
|
18102cf0a9 | message default (#66) | ||
|
|
b1cab32b48 |
chore: local runs (#64)
* open runs * open runs * ci |
||
|
|
f1df93d4a3 |
Codex Auto: sandbox escalations become permission cards (PR2 re-land, v0.1.52) (#63)
* Codex auto mode: grant the orx data dir as a sandbox writable root workspace-write denies writes outside the workspace, and the orx SQLite store lives under ~/.local/share (WAL + read-write open), so every store-touching orx command died with 'unable to open database file' under Auto mode. Add store::data_dir() to the turn's writable_roots alongside the shared git dir. * Review fixes: pin ORX_DATA_DIR to the granted root, ensure_ naming - Pin ORX_DATA_DIR on the codex child (after prepare_env) so the store the sandboxed orx resolves is the exact path granted as a writable root — synced env or a relative override could otherwise diverge. - Rename orx_data_dir -> ensure_orx_data_dir (codebase ensure_ prefix for create-if-missing helpers); note the grant is dir-wide. - Dedent the '-c replaces user roots' note: it covers both roots. * Review round 2: match-expression pin, stale options() doc, asymmetry note * Codex chat: migrate to app-server protocol with exec fallback One long-lived 'codex app-server' child per session (CodexHost, the AgentHost pattern), JSON-RPC over stdio. Threads persist as the session's native id and resume across orx up restarts; the playbook rides developerInstructions; the sandbox policy travels per turn as a first-class sandboxPolicy param (writable roots + network) instead of -c config overrides that clobbered user config. approvalPolicy stays 'never' on every mode — this is a pure transport swap; the follow-up flips Auto to on-request and surfaces approval requests as permission cards (validated live against codex-cli 0.144: sandbox escalations DO arrive as item/commandExecution/requestApproval, upstream #21982 does not reproduce). codex < 0.144 keeps the legacy exec path for one release (ORX_CODEX_EXEC=1 forces it); detect() notes the upgrade. * Review round 1 fixes: turn-id filter, drop race, bounded interrupt Majors: - Filter turn events by the turn/start response's turn id so an aborted predecessor's tail (fatally, its turn/completed) can't leak into a successor turn's transcript. - TurnRoute::drop only detaches its own channel (same_channel guard) — an aborted task's late drop can no longer clobber a successor's registration. - interrupt_active_turn bounded to 5s so a wedged child can't hold the user-facing interrupt/delete paths hostage; reader EOF now also kills a half-dead child instead of leaving a zombie in the registry. - thread/resume falls back to a fresh thread only on a server-reported error (try_request); transport failures propagate so a resumable thread is never discarded over a timeout. - delete_project now kills codex children too (was opencode-only). Nits: options()/sandbox_policy_json doc rewrites, tool_part/tool_status/ part_exists/find_codex_required/respond_decline helpers, HashSet for unanswered ids, REQUEST_TIMEOUT const, outputDelta creates its part, turn/completed(inProgress) guard, double-error dedup, truthy ORX_CODEX_EXEC, softer agent_note, import cleanups. * Review round 2 fixes: abort-proof child bring-up, shared-helpers section Major: spawn + register + handshake now run in a detached task, so an aborted turn task (interrupt/delete during the first message) can no longer orphan an unregistered app-server child; a no-displace insert keeps a raced successor's live client intact, and handshake failure kills + deregisters only its own entry. Nits: shared helpers moved out of the legacy section under their own banner; set_active_turn armed from the turn/start response (deletes the turn/started special case and closes the early-interrupt no-op window); parse_codex_version takes the first parseable token; resume-rejection logged; HANDSHAKE_TIMEOUT const; TurnEnd/apply_notification docs; has_error_part test; fold-test guard tightened; comment accuracy. * Round 3 polish: doc word order, eprintln prefix, mid-handshake reuse note * fmt * Codex Auto: on-request approvals surfaced as permission cards Auto now runs approvalPolicy on-request: the writable roots keep routine orx traffic prompt-free, and anything escalating past the sandbox arrives as item/*/requestApproval, surfaced as a permission card (native_id = JSON-RPC request id) and answered inline via resume_from_prompt -> {"decision": accept|decline}. Invariant: no unresolved codex card outlives its turn — every turn exit sweeps open cards (resolve + decline with codex); interrupt paths cancel pending approvals natively before turn/interrupt; serverRequest/resolved reconciles codex-side settlement (its ~5min approval deadline). Stale-turn requests are declined, never surfaced; Bypass auto-accepts; unknown request types get a JSON-RPC error instead of blocking codex. A 10-minute no-event watchdog (suspended while a card is pending) interrupts wedged turns. * PR2 review round 1 fixes: stale-card defense, Bypass reply schema, card content - Consent integrity: codex request ids restart per child, so a stale unresolved card could deliver a click to a different live request. Turn-entry resolve_stale_prompts sweep (store-side, codex-only — end-turn harnesses keep their cross-turn cards) + turn-scoped part ids (appr-{turn}-{id}) close it from both ends. - Bypass now auto-accepts only card-able approval methods; unknown request types get the JSON-RPC error reply in every mode (a {decision} reply to item/tool/requestUserInput would fail to parse). - Permission cards show the request's human-readable reason (fileChange cards carry nothing else); UI dist rebuilt. - serverRequest/resolved also clears the id from the client's pending set (stale-answer guard stays truthful after codex self-settles). - Reconciliation computes the part id directly (approval_part_id) instead of a reverse scan; resolve_card returns (); sweep declines unconditionally (respond's pending set is the single arbiter); watchdog bumped to 30min with an honest false-positive note; resume_from_prompt gains a kind guard. * PR2 round 2 polish: schema-aware fallback replies, immediate card flush Non-approval request methods now get the JSON-RPC error reply in every fallback path (read_loop unrouted, stale-turn arm) via a shared is_approval_request; codex-settled cards flush immediately instead of waiting for the next event; message_json reuse; Handled doc drift; watchdog comment dedup. * Bump version to 0.1.52 |
||
|
|
6636e8e8cc |
Files tab redesign: minimalist tree + proper report rendering (v0.1.51) (#58)
* Files tab redesign: minimalist tree, centered column, GFM report tables * Bump version to 0.1.51 * Review round 1 fixes: GFM link/table edge cases, shared code blocks, class rename sweep * Rebuild ui/dist |
||
|
|
a0e67cd210 |
File viewer: open files from the chat session's worktree (#57)
Clicking a file chip in chat failed with "path must be repo-relative": agents work in per-session worktrees, and the viewer only knew how to strip the hub-clone prefix — and only served files from the hub clone, where agent-created files don't exist. - App.tsx parseFilePath maps agent-reported paths (relative, repoPath- prefixed, worktrees/ and repos/ cache layouts) to a repo-relative path plus the session id; relative paths inherit the click context's session (chat's active session, or the viewed file's). - project_file gains a sessionId query param: the session is validated against the store (must belong to the project), its worktree is served when present, and a missing worktree degrades to the hub clone. The response's root field says which checkout answered so the viewer can flag the fallback. - File tabs are keyed by (path, sessionId) so the same path from different sessions opens distinct tabs. |
||
|
|
10eaa5a25e |
Recents: hover kebab menu (archive/delete) + Active/Archived/All filter (#56)
* Recents: hover kebab menu (archive/delete) + Active/Archived/All filter
Claude-desktop-style session management in the chat rail:
- three-dot menu on session-row hover with Archive/Unarchive and Delete
(confirm-guarded; delete already existed server-side)
- filter button beside the Recents label: Active (default) / Archived / All
- new archived column on chat_sessions + PATCH /api/chat/sessions/{id};
archive keeps updated_at so sessions keep their recency slot
* Review round 1: delete broadcast, keyboard access, rollback + polish
- delete_session now emits chat.session.deleted; dashboards remove the row
(and its cached transcript/busy state) instead of keeping ghosts
- set_archived re-reads after the write so the broadcast can't clobber a
concurrent title change or resurrect a deleted session; sending into an
archived session unarchives it (activity unarchives)
- kebab reveal uses :focus-within (row-only :focus-visible dropped focus to
body mid-Tab); row keydown ignores events from descendant buttons
- archive rollback restores the pre-request snapshot, not the negation;
removeSession alerts on failure and uses functional setActiveId
- Escape closes all popovers; dead .rail-filter-btn.active rule removed;
danger color uses --accent-red; copy/labels tidied
* Review round 2 polish: delete tombstones, keyboard menu dismiss, error surfacing
- deletedIds tombstone set ignores chat.session upserts racing a delete
(a naturally-finishing turn could resurrect a ghost row)
- row Enter/Space mirrors the click branch: dismiss an open menu instead of
switching sessions underneath it
- deleteChatSession goes through the shared json helper so the alert shows
the real server error; alert copy names the session
- one filter reset in send() covers new sessions and unarchive-on-activity
- SESSION_FILTERS carries railLabel (single table); comment placement fix
|