[pull] dev from menloresearch:dev - #129
Open
pull[bot] wants to merge 2359 commits into
Open
Conversation
…chunk sizes fit its context getDefaultEmbeddingModelId/setDefaultEmbeddingModelId read/wrote raw webview localStorage, but the Settings UI's default-embedding-model store persists through the Rust settings backend on desktop. The extension's embed() never saw the user's choice and silently fell back to sentence-transformer-mini (256 ctx) every time. Also add a real safety net: char-based chunking can't reliably predict token count across tokenizers/content, so vector-db-extension now verifies each chunk against the embedding model's actual /tokenize output and recursively halves any chunk that still exceeds context, instead of relying on a fixed chars-per-token estimate.
Three client-controlled invalidators were reshaping the prompt prefix every turn, forcing llama-server to reprocess from token 0 instead of reusing its cached prefix (reuse is pure longest-common-prefix matching): - cache_prompt was never sent, relying on the server default; assert it explicitly so a preset/CLI override can't silently disable prefix reuse. - Chat requests carried no id_slot, so with parallel>1 consecutive turns could land on an empty slot. Pin chat to slot 0; title generation keeps using the reserved background slot and can't evict it. - Smart tool routing selected tools from the latest user message, changing the tool set (which serializes ahead of history) each turn. Freeze the routed set for the thread's lifetime, re-routing only when the connected servers or disabled-tool set changes.
Redesign the reasoning/CoT trace to reduce the vertical space it consumed while streaming and to read as a clean timeline: - While streaming, show only the last completed reasoning paragraph as a single bounded block that swaps (with a fade/slide transition) as each paragraph finishes, instead of the whole growing trace. The in-progress paragraph is not shown; the header conveys activity. - Once done, the full trace renders as a single dotted-rail timeline, one dot per paragraph (splitReasoningParagraphs), fixing the doubled rail that came from ChainOfThoughtContent's border plus the step connectors. - The CoT container is a bordered card only while expanded and collapses to the bare 'Thought for N seconds' summary.
On reload (notably mid-stream, when the llamacpp router keeps the extension layer busy), DataProvider's one-shot fetchThreads could run before the Conversational extension registered. fetchThreads returned [] for that not-ready state, so setThreads([]) wiped the sidebar list and it stayed empty until the next clean reload (also surfacing as a transient 'Error in route match: __root__/'). fetchThreads now throws when the Conversational extension is unavailable, distinguishing 'not ready' from 'no threads'. DataProvider retries with backoff (up to 20 attempts) and only writes on success, so a failed attempt never overwrites a populated list.
Map Jan's reasoning settings to each cloud SDK's native reasoning option
(no invented token budgets) and surface the control in the composer:
- New buildReasoningProviderOptions() → per-request providerOptions:
Google thinkingConfig (dynamic -1 / off 0 + includeThoughts), Anthropic
adaptive/disabled thinking, OpenAI reasoningEffort + reasoningSummary.
Threaded into the streamText call in custom-chat-transport.
- ChatInput reasoning popover ungated to gemini/google and anthropic
(Auto/On/Off; token-budget submenu stays local-only), plus a discrete
effort picker (Default/Low/Medium/High/XHigh) for openai.
- model-factory: OpenAI uses the Responses API only when an effort level
is set (so reasoning summaries stream) and stays on chat/completions
otherwise, avoiding breakage of OpenAI-compatible proxies.
Note: Anthropic 'off' relies on the SDK omitting the thinking field
(= model default); the SDK drops {type:'disabled'}, so a model that
reasons by default can't be forced off via providerOptions.
First phase of turning the proxy into a translating gateway. Adds the
plumbing and the shared primitive without changing any behavior:
- ProviderConfig/RegisterProviderRequest gain an optional api_type
("openai" default = verbatim passthrough; "openai-responses"/"google"/
"anthropic" will select a converter in later phases).
- New core/server/converters.rs with SseAccumulator, which reassembles
complete Server-Sent Events across network chunk boundaries. The existing
inline proxy parser splits each chunk on lines and drops partial lines;
every native-response translator needs correct cross-chunk framing, so
this is the load-bearing primitive to build converters on.
No provider is routed through a converter yet. cargo test --lib (8 new
SSE tests), clippy -D warnings, and cargo check all clean.
Add UpstreamConverter trait + OpenAIResponsesConverter: rewrites chat/completions -> /v1/responses (messages->input, system->instructions, reasoning_effort->reasoning, max_tokens->max_output_tokens, tools flatten) and translates the native response/stream back to chat.completion, emitting reasoning_content for reasoning summaries. Wired into proxy.rs behind ProviderConfig.api_type='openai-responses'; absent/openai keeps verbatim passthrough. Streaming uses SseAccumulator for cross-chunk event reassembly. 10 new converter tests.
Add GoogleGenerateContentConverter behind ProviderConfig.api_type='google'.
Request: messages->contents (assistant->model, tool->functionResponse keyed
by name via a tool_call_id->name map), system->systemInstruction,
reasoning_effort->thinkingConfig{thinkingBudget:-1,includeThoughts}, tools->
functionDeclarations, tool_choice->functionCallingConfig.mode. Response/stream:
candidates parts -> content/reasoning_content (thought parts)/tool_calls;
usageMetadata + finishReason mapped.
Widen UpstreamConverter: upstream_path(body) (model+action in URL, ?alt=sse
for streaming) and auth_header (Google uses x-goog-api-key, not Bearer);
proxy wiring + OpenAI impl updated. Fix chunk_str_with_usage to set already
chat-shaped usage verbatim. 10 new Google tests.
Add AnthropicMessagesConverter behind ProviderConfig.api_type='anthropic' (OpenAI-in -> native /v1/messages-out; orthogonal to the existing inbound /messages Anthropic->OpenAI path, which is untouched). Request: system messages -> top-level system, assistant tool_calls -> tool_use blocks, tool messages -> tool_result blocks, consecutive same-role messages merged (Anthropic rejects non-alternating roles), tools -> input_schema, tool_choice mapped, max_tokens defaulted (Anthropic requires it). Response/stream: text/thinking/tool_use blocks -> content/reasoning_content/ tool_calls; stop_reason + usage mapped; input_tokens carried from message_start. Trait: add extra_headers hook (Anthropic anthropic-version) + auth_header x-api-key; StreamState gains input_tokens. Proxy applies extra_headers in the key loop. Reasoning intentionally not auto-enabled (Anthropic thinking needs an explicit budget_tokens we can't infer). 11 new tests.
createOpenAIModel now returns openai.responses(modelId) unconditionally instead of only when a reasoning effort level was set. Responses is a superset of Chat Completions and the only surface returning reasoning summaries; genuine OpenAI always supports it. Custom OpenAI-compatible providers route through createOpenAICompatibleModel, not here, so this cannot hit a proxy that only implements /chat/completions. Drops the now-unused effortLevel lookup + isThinkingBudgetLevelKey import.
The Responses-API-only OpenAI path (56c8724) broke three test mocks that only stubbed openai.chat, not openai.responses.
…ptions buildReasoningProviderOptions returned Record<string, Record<string, unknown>>, which the AI SDK's streamText providerOptions (SharedV3ProviderOptions = Record<string, JSONObject>) rejects, breaking the tsc build.
feat: OpenAI-compatible translating gateway + Responses API + unified reasoning
Add a regression test that drives CustomChatTransport.sendMessages twice on one thread and asserts the system prompt, tool set, and prior model messages are re-serialized byte-for-byte before being sent to the provider, guarding the KV-cache prefix reuse the transport relies on. Bump app version to 0.8.4.
…he UI listThreads() swallowed errors into [], and a not-yet-ready extension race could overwrite an already-populated list with an empty result. Let failures propagate so the retry loop kicks in, skip empty-result overwrites when threads are already loaded, and re-arm the fetch on late extension registration.
…r and thinking options
Three regressions from the v0.8.4 reasoning/SSE work:
- filterDefaultSseEvents was applied unconditionally in createCustomFetch,
deleting every named SSE frame. Anthropic (message_start/content_block_delta)
and the OpenAI Responses API (response.output_text.delta) stream exclusively
named events, so Anthropic errored and OpenAI rendered empty assistant
messages; Gemini's data-only frames passed through. The filter is now opt-in
(filterNamedSseEvents) and enabled only for the llamacpp, MLX, and
openai-compatible factories it was written for.
- Anthropic thinking is now gated by model generation: adaptive exists on
4.6+ only and `display` shipped with 4.7, so pre-4.6 models get
{type: enabled, budgetTokens} mapped from the thinking-budget level
(the SDK adds the budget on top of max_tokens) and the 4.6 family gets
adaptive without display.
- An empty api_key fell back to apiKey: '' which puts an empty auth header
on the wire; Anthropic answers that with a misleading "x-api-key header is
required" 401. requireRemoteApiKey() now throws an actionable
"No API key configured" error instead.
swift-jinja 2.4.0 changed Value.object keys from String to ObjectKey, which fails to compile against swift-transformers 1.3.3's jinjaValue(). The transitive dep was unpinned (from: 2.0.0), so CI drifted onto 2.4.0. Constrain it directly in the root manifest to the last-good 2.3.x line.
…rontend logs to app.log Introduce a single logging path across the web-app, core, and extensions. - core: add shared `logger` (levels -> console.*) + `formatLogArg`, no Tauri dep - web-app: install console interceptor in main.tsx boot that forwards console.* to app.log via tauri-plugin-log (release: warn/error/info; debug builds: also debug/log); reuses core's formatLogArg - llamacpp/mlx: drop direct @tauri-apps/plugin-log usage (fixes double logging now that the interceptor forwards console.*) and use shared logger - assistant/rag/download/core: migrate raw console.* to shared logger - demote provider-registration logs to debug (DataProvider.tsx + remote_provider_commands.rs) - tests: add logger to @janhq/core mocks, rewire util.test assertions
…d setting - Render the finished reasoning trace as one continuous dotted rail so a tool call between two reasoning paragraphs stays threaded instead of restarting the rail. StepRow now hosts arbitrary content; dead ReasoningTimeline removed. - Remove the "Fold interim text into reasoning" setting and its fold-mode code path; interim answer text between reasoning steps always renders as a normal message.
…off localStorage - Add "Auto-generate chat title" interface setting (default on). When off, the thread-title summarizer is skipped and the llama-server router reserves no extra background parallel slot (RESERVED_BACKGROUND_SLOTS drops to 0). - Fix llamacpp-extension reading web-app stores from localStorage: proxy config and the model-provider blob live in settings.json (backendStorage) on desktop, so the reads returned null — ignored proxies and a no-op per-model YAML backfill. Route all persistence through a new backend-settings helper (settings_get/set/remove); migrate the backend-type preference and every migration/backfill done-marker off localStorage too. - localStorage is now used only for the one-time pre-backend settings migration, enforced by a guard test (no-localstorage.test.ts).
Pressing stop escalated to force_stop_model -> POST /models/unload, freeing the model from VRAM and discarding its KV cache, so resending forced a full model reload and prompt re-ingestion. The escalation existed because aborting the HTTP request alone did not stop generation: tauri-plugin-http 2.5.2's fetch_read_body drained the response body with no cancellation, keeping the llama-server connection open (with max_tokens=-1 an infinite thinking loop never ended), so the router never saw a client disconnect. Bump tauri-plugin-http to 2.5.9 (JS x3 + Rust crate), which reads the body chunk-by-chunk and calls fetch_cancel_body on abort, dropping the connection. The router then observes the disconnect and stops generation with the model still loaded and KV cache intact, so resending reuses the prefix cache. This pulls the Tauri stack to 2.10.3 (no new cargo-audit advisories; resolves RUSTSEC-2026-0009). Remove the now-dead force_stop_model command and the escalation in ChatInput (which also violated the guest-js invoke rule).
… reconcile vector-db ACL - setup: gate extension install on manifest integrity (files referenced by extensions.json must exist), so a missing/empty/partially-deleted extensions dir self-heals on launch instead of booting with broken extensions - system: drop stale jan.exe before rename on Windows so a version change reliably overwrites the CLI binary (Unix already overwrites via fs::copy) - vector-db: remove orphan memory_* permission artifacts so the regenerated ACL manifest matches build.rs (11 real commands) under Tauri 2.10's stricter resolver
fix: Stabilize chat prefix caching, restore cloud providers, unify logging (v0.8.4)
…ers (#8435) * fix(mcp): guard against duplicate serve() on streamable-http servers Remote streamable-http MCP servers (type "http") intermittently failed to connect with `400 Bad Request ... when send initialize request`, or connected then dropped a few seconds later. Root cause: `start_mcp_server` had no idempotency guard, so a server could be serve()'d twice (e.g. a rename fires activate + syncServersAndRestart, and `restart_active_mcp_servers` restarts active servers without clearing them first). Each serve() spins up an independent rmcp client that sends its own `initialize` (both with request id 0); the second is rejected by the server as a duplicate on an already-initialized session, tearing down the connection. Fix: make `start_mcp_server` idempotent — skip if the server is already running or a start is already in flight (tracked via a new `mcp_starting` set on AppState), clearing the in-flight marker when the attempt completes. The reconnect path (`schedule_mcp_start_task`) is intentionally left unguarded so auto-reconnect can still re-serve. Fixes #8411 * fix(mcp): log duplicate-start skips at debug level Address review: the idempotency-guard skip messages are routine, not noteworthy, so log them at debug instead of info.
* fix(providers): send anthropic-version on model-list fetch Custom Anthropic-compatible providers (e.g. an anthropic proxy) failed to fetch their model list: the GET /v1/models request omitted the anthropic-version header that the Anthropic API requires, producing a 400 surfaced as the generic 'check your API key and base URL' error. The built-in anthropic provider worked only because it ships the header as a baked-in custom_header, which user-created providers cannot obtain (no custom-header UI). Add ensureAnthropicVersion() which sets anthropic-version: 2023-06-01 unless already present, applied on both model-fetch header paths (tauri fetchModelsFromProvider and remoteModelCatalog buildHeaders). Harmless to OpenAI/Gemini and other providers, which ignore the unknown header. Fixes #8410 * fix(providers): gate anthropic-version header to anthropic providers Only inject anthropic-version when the provider fronts Anthropic (built-in anthropic or an Anthropic-compatible proxy, detected by provider name or host) rather than on every provider's model-list fetch. * fix(providers): make custom Anthropic providers work end-to-end Custom Anthropic providers (api_type=anthropic, custom name/host) failed because header injection and catalog support keyed off a name/host substring instead of the api_type discriminant the inference dispatch already uses, and because custom providers ship no custom_header so the browser-access opt-in was missing. - Gate anthropic headers on api_type first (name/host as fallback). - Inject anthropic-dangerous-direct-browser-access alongside anthropic-version; the webview is a browser context and Anthropic 401s Origin-bearing requests without it. Applied to catalog fetch, model-list refresh, and chat inference (createAnthropicModel). - Honor api_type in supportsRemoteCatalog/fetchTopRemoteModels so custom-named Anthropic gateways populate the hub catalog.
…8611) Add a show_reasoning toggle to the agent TUI that folds the model's thinking blocks behind a [thinking] indicator while streaming, and lets the user expand them inline. On the live view a reasoning block shows [thinking] while open and [thought for Ns] for a short TTL after it closes (falling back to the plain [working]). Also fix a stale ToolPermissions import in the max-parallel test that moved into tauri_plugin_agent_tools and clear several cli-build warnings: unused Mutex import, unused r1 binding, and the now dead set_skills_enabled_in_agent_toml under the cli feature.
…on gate (#8614) The OS jail (bubblewrap/seatbelt/AppContainer) confines every shell command regardless of the permission gate, so prompting on each write and exec is no longer what contains the CLI agent. Flip the default: tool calls are auto-approved, and --safe turns the interactive gate back on. - Remove --yolo from the bare TUI, `cli agent run`, and `cli agent step`; add --safe, inverted once at the arg boundary. - Rename OrchestrationArgs.yolo to auto_approve. Desktop entry points keep it false, so desktop behavior is unchanged. - Drop the --yolo stderr banner, which would now fire on every default run. The TUI notes the active mode on startup instead. - HardDeny is untouched: .jan/agent/ internals, [tools] deny, and plan mode still refuse regardless of auto-approval. Docs restructured around Default/Safe/Plan. The CI example drops the flag and warns that --safe with no TTY auto-denies and stalls on the first write. Also correct two claims this invalidated, plus one that was already wrong: allow_network's rationale no longer cites the prompt, the generated agent.toml template says "go through the permission gate" rather than "still prompt", and permissions.mdx no longer says the sandboxed shell has no network by default (true on desktop, false on the CLI, where DEFAULT_ALLOW_NETWORK is true).
* fix(agent): count marginal token spend in session budget (#8613) The session budget summed each completion's absolute total_tokens. Since every turn replays the full accumulated conversation, total_tokens per turn equals the whole growing context, so summing it grows quadratically with context length and cut off legitimate long tasks with "session token budget exhausted" before they finished — while a true runaway could burn the same cumulative totals undetected. Track the marginal increase between requests instead, so the ceiling guards real new spend without penalizing the unavoidable context replay. Compaction can shrink the replay below the prior total; saturate at zero so it never refunds spend. Fixes #8613 * fix(agent): charge completion tokens after compaction
Tool group and standalone rows previously only checked the last call's status and lagged until the group closed, so an aborted run (cancel, error, dispatch_subagent racing its result) could leave a "->" running marker or a false "check" success on calls that never resolved. - ToolGroup::is_running now scans all calls, not just the last, since results within a batch can land out of dispatch order. - Add ToolGroup::outcome_tag/row and a GroupRow state (Open/Closed/ Aborted) so a failed call outranks an unresolved one, and an aborted group renders unresolved calls with a distinct "o" marker instead of a false checkmark. - refresh_group_row updates the open group's row as each result lands instead of only at group close. - Track standalone (diff-producing) call rows via pending_rows so abort_tool_rows (replacing finalize_tool_group at cancel/error sites) can mark any still-open row interrupted rather than leaving a stale "->". - "Preparing X: path" now names the actual tool instead of hardcoding "write".
…ound (#8617) * feat(agent): drop turn caps, make the session token budget the only bound max_turns and budget.max_steps were near-duplicate ways to bound a run, and only one of them was ever wired: [budget] was absent from AgentToml, so max_steps (and budget.max_tokens) were written by /settings and the scaffold template but never read. Meanwhile SessionBudget had a live consumer with nothing feeding it -- the CLI never sent max_session_tokens, so every run was effectively unbounded. Remove the turn caps entirely and wire the budget instead. The agent takes as many turns as the task needs; cumulative token spend (or cancellation) is what stops a runaway loop. Removed: - [agent].max_turns, and max_turns/max_steps from the scaffold template - --max-turns on both `jan` and `jan cli agent run` - DEFAULT_MAX_TURNS and the max_turns_override plumbing - the max_turns and budget.max_steps rows in /settings Wired: - [budget].max_tokens -> max_session_tokens on the request body -> SessionBudget, defaulting to 128000 when unset; an explicit 0 disables the ceiling. Counted marginally (see #8615), so it bounds real new spend rather than the context replayed each turn. - loop.rs treats an absent max_turns as unbounded (was 8), which also lifts the silent 8-turn cap the desktop chat path was hitting. Two paths keep a turn cap deliberately, since neither can be cancelled mid-run: `jan cli agent step` sets max_turns=1 on the body directly, and the API-server proxy injects PROXY_DEFAULT_MAX_TURNS=8 when a client omits it, preserving the documented openapi.json default. Also groups the per-run numeric knobs into SessionLimits rather than adding a tenth positional arg to App::new beside three existing bare numbers. Unrelated fixes in code this touches: dotted_keys_write_and_remove_under_their_section failed to compile under --features test-tauri (it calls the cli-gated set_agent_key from an ungated test), and the unique_root test helper keyed temp dirs on tag + per-process counter alone, colliding across the two test binaries. * docs(agent): drop max_turns/max_steps docs, make token budget the only bound The head commit removed max_turns/--max-turns and budget.max_steps, wiring [budget].max_tokens as the only cap on run length (default 128000, 0 disables). Update project-config, cli, context, and slash-commands pages that still described the removed surface.
…th (#8619) Committed transcript entries were rendered once, at the width in effect when they were appended, so a resize left markdown tables, boxed diff panels and truncated tool labels sized for the old terminal. `transcript` becomes a `Vec<Row>`: each row keeps its source (`RowKind::{Line, Markdown, Tool, Result}`) and renders to lines at the width of the frame it is drawn into. Diffs re-box and re-highlight, prose re-wraps, and tool labels -- now stored untruncated -- re-clamp, growing back when the terminal widens. A row may render to several lines, so transcript indices (group, reasoning, subagent, pending-row, `expanded`, `reveal`) address rows and survive a resize; `draw` repeats each row's index in `row_index` so click mapping still resolves. `view_width` moves to the top of `draw`, before the todo HUD reads it to size the layout. `Row::lines` memoizes per width: the whole transcript is laid out every frame, so re-parsing markdown and re-highlighting diffs 20x/sec costs 16.9ms/frame at ~500 rows vs 14.1ms with the memo (14.05ms before this change). Rows are replaced wholesale, never mutated, so it cannot go stale.
…8620) A group row marked itself failed if any folded call errored, so a call that failed and then succeeded on a retry kept reading as failed. Track the most recently arrived result on the group (results can land out of dispatch order, so it can't be read back off `calls`) and tag the row from that. The failing call keeps its own marker in the expanded detail.
…e input (#8625) The plan and the running subagents describe now, not what was said, so they no longer live in the transcript. `status_panel` renders them into a slot between the body and the separator rule: - Two columns joined by `join_columns` at width/2 (`todo_column` left, `agents_column` right), stacking below `PANEL_SPLIT_MIN_WIDTH`. - `panel_budget` bounds the slot by what is left after the header, rule, input and dock plus `MIN_TRANSCRIPT_ROWS`, capped at `PANEL_MAX_ROWS`, so a short terminal loses panel rows and never conversation. Each column elides its tail with a `+N more` count. - The plan column expands only the phase in flight and keeps compact stats; the agents column shows the newest call per agent, with detail rows as budget allows (`AGENT_MAX_ROWS`). - An `await_subagent` whose run has a live panel prints no waiting row. - `[thinking]` shimmers (`shimmer_spans`, `SHIMMER_PAUSE`) since folded reasoning is otherwise silent on screen; other badges stay flat and the label width is fixed. - `gap` compares activity bands, so folded reasoning next to tool rows spends no blank; the borderless input reserves `content + 1` rows; and `dock_line` merges the path with the footer hints onto one row. Both halves also give their rows back once they stop being live state. A finished plan hides after `TODO_HIDE_AFTER` (3s): `refresh_todo_deadline` stamps `todos_closed_at` from `draw`, once per frame rather than at each of the four `todos` write sites, and clears it the moment work reopens. It has to be wall-clock -- `turns_since_todos_closed` only advances on model roundtrips, so a run that finishes its plan and stops would pin a fully checked-off list forever. Hiding is display-only: `/todo` still opens the list and `age_closed_todos` still owns clearing it. The fan-out needs no timeout, since each child's `SubagentEnd` already moves it into the chat as a summary row -- but that was the only thing closing a panel. A run ending without one (upstream error mid-fan-out, a `Done` beating the child's event) stranded the block, spinning in the dock on an idle session until the next `/clear`; `cancel_run` cleared it, but silently. `close_live_subagents` now runs from `on_done`, `on_error` and `cancel_run`: a child cannot outlive the run whose stream carries its events, so any panel still open is closed and summarized as `interrupted`, sharing `push_subagent_summary` with the clean path so the two cannot drift.
Up/Down now recalls previously submitted lines into the composer when the buffer is empty; otherwise they still scroll the transcript, and PageUp/ PageDown always scroll so scrollback stays reachable. Recalling steps back through the session's history, past the newest entry returns to a fresh message, and a resend of the same line dedupes to a single history entry.
#8621) Bracketed paste while an ask prompt is open went to the chat composer, so a pasted custom answer never reached the ask responder and the model saw ERROR [ask_cancelled]. Route paste into the active custom answer field; paste during option selection is dropped so it cannot leak into the next chat message. Repeated key events (keyboard auto-repeat) were previously rejected by the ask key handler, so holding Enter did nothing. Ignore only key-release events now. Tests drive the real handlers end to end: repeated Enter selection, paste into the custom editor, and a custom answer received at the agent invoker boundary.
…agent (#8637) * fix(agent-tui): show the full command on a single bash tool row A lone in-flight call is specific enough to name, so the live group row now shows its own label instead of the counted "Running 1 command" breakdown; two or more calls still summarize. The bash labels also drop their fixed 80-char cap on both the running and finished rows -- the row already clamps to the draw width, so the command fills the terminal and grows back on a resize. * feat(agent-tui): track the mouse by default and own drag-to-select Mouse tracking is on at startup with no hotkey; `mouse = false` in ~/.jan/config.toml turns it off and hands selection back to the terminal. Ctrl-T, App::mouse_capture and the per-tick enable/disable diffing in chat_loop are gone. Alternate scroll (1007) is saved, forced off and restored on exit: it made the terminal translate the wheel into arrow keys, which the composer read as message-history recall. Tracking itself is requested by hand rather than via crossterm's EnableMouseCapture, which also asks for any-motion reporting (1003) -- an event per idle pointer move. We take buttons and wheel (1000), motion while held (1002) and SGR coords (1006). Tracking takes selection away from the terminal, so the TUI implements it: drag selects with no modifier and copies on release, Alt+drag takes a rectangle, and a drag held past an edge scrolls a row per frame while the anchor follows the content. Press and release are split so a drag that starts on a folded row selects instead of expanding it. The highlight is a post-pass over the finished frame, so it covers every surface; the text is lifted from that same buffer one frame later (only draw holds one) and copied via arboard plus OSC 52, so it survives jan exiting. Recall is rebuilt from the conversation (rebuild_recall) rather than kept as an independent log, fixing two ways the two could drift: a resumed thread had no recall at all, so Up scrolled instead, and a rewind left entries for messages it had just dropped. * fix(agent): hand back partial progress when the session budget runs out Exhausting the session token budget mid-turn returned an Err, which failed the whole run. A subagent inherits the parent's remaining budget, so it was the likeliest caller to hit the ceiling -- and its partial work was lost with the error instead of reaching the parent that asked for it. Treat exhaustion as a soft stop: append an assistant message describing the progress so far, publish MessagesUpdated, and return a synthetic completion with finish_reason "stop". Tool calls still do not run, so nothing further is spent against the ceiling. * fix(agent-tui): run compaction off the render loop with a throbber /compact awaited the summarizing model call inline in chat_loop, so no frame was drawn for the whole round trip and the TUI looked hung. Auto-compaction blocked the same way, and papered over it with a note it later scrubbed back out of the transcript by matching row text. Both paths now hand a CompactKind to the loop, which spawns the call and joins it in select! (the login/update pattern), so the 50ms tick keeps painting: the header badge reads compacting/auto-compacting and the input row carries the spinner plus elapsed seconds. Row::plain_text goes with the scrubbing hack. Off-loop means history can change under the call, so: a run will not start while a compaction is in flight (its result replaces history wholesale), and finish_compaction measures against the length the summary was computed from and re-appends anything queued since, instead of dropping it. * fix(agent): force todo plan only on active /goal runs Narrow the eager todo-plan addendum to /goal runs: an unattended loop that keeps firing turns needs a phased list staged up front, while an ordinary turn leaves the model free to decide. Replace the first-message word-count heuristic (should_suggest_eager_todo_plan) with should_force_goal_todo_plan, gated on a goal_mode body flag and an empty registry. The upkeep addendum still applies on any turn with a list. Drop the now-unused user_message test helper. * feat(agent-tui): forward goal_mode in the run body Body() sets goal_mode true only while an active /goal runs, so an achieved or cleared goal reverts to an unchanged body and ordinary turns never advertise it. Single concern split out on its own for review. * fix(tui): resolve standalone tool rows to past tense once result lands * fix(tui): recover pasted images from the clipboard file list macOS Finder copies publish a file URL rather than raster data, so `get_image` fails and the paste was only recovered when `get_text` happened to return an absolute path. Fall back to arboard's `file_list`, which also covers Explorer and Linux file managers. The list is whatever the user copied, not necessarily an image, so gate entries on a known image extension (`image_mime_of`, with `image_mime` keeping its PNG default for explicit `/image` paths) and cap `load_image_file` at MAX_IMAGE_BYTES, checked via metadata before the read. Entries that fail either check are skipped rather than ending the search. * fix(agent-tui): drop the duplicate summary above a standalone diff A resolved standalone tool row already names the tool and file in past tense ("Edited tui.rs"), so the result's own "Applied N edit(s) to X" line only repeated it directly above the diff panel. `RowKind::Result` now takes an optional `content`, and the summary line is skipped when the call row was rewritten, the call succeeded and a diff panel follows. Errors keep their text: the row says nothing about why the call failed.
* fix(agent): preserve compaction checkpoints * test(agent): preserve file tool context in compaction
* fix(agent-tui): size boxed diff and code panels to the draw width `boxed_panel` spends `gutter + 4` columns on its frame, but the transcript call sites reserved a flat `width - 8` while their gutters are 6 and 8 chars wide. Every panel row came out 2-4 columns too wide, so the closing border wrapped onto a line of its own: no right edge and a blank row after every content line. `diff_lines`/`boxed_panel` now take the total draw width and derive the content width through a shared `panel_inner`, removing the duplicated `- 4` from the result row, the expanded group detail, the permission preview and the markdown code block. * feat(agent-tui): resume the whole session, not just its text /resume restored only user/assistant text: reasoning, tool calls, their results and diff panels were gone, and the model came back unaware of the work it had done. Two files now back a resume. display.jsonl (core::cli::journal) is the transcript as rendered, in emission order, dumped by a single background writer thread from persist -- so every turn boundary and every cancel leaves a resumable journal without a frame paying for the write, and concurrent renames cannot land a stale one. replay_display_log feeds it back through the same functions that rendered it live, so there is no second renderer to drift. It cannot be folded into messages.jsonl: reasoning is deliberately never resent to the model, and /compact rewrites the wire history without touching what the user can see. messages.jsonl now carries tool_calls/tool_call_id through as extra keys on the thread.message record, and rebuild_wire_history reconstructs the conversation for both /resume and `jan cli agent run --resume`, enforcing tool pairing: an orphaned result is dropped, and a call whose result never reached disk gets an explicit placeholder rather than an invented outcome. Also: on_done/cancel_run strip <think> from the history they push, which the streaming layer always intended (reasoning is display-only) but the TUI had been resending and persisting; rewind replays the journal so kept turns keep their tool rows; and apply(ToolCall) flushes buffered prose once at the top of the arm instead of per branch, which both removes a duplicated call and keeps the journal in emission order. * feat(agent-tui): open on a splash instead of two dim notes The first frame was an empty screen with two dim status lines at the bottom, which said nothing about what jan is or how to drive it. Open with the same wordmark `jan --help` prints, followed by the facts a first-time reader needs -- model, project and branch, how tool calls are approved -- and the commands to go on with. The art now lives in one place (`core::cli::brand::LOGO`), shared by the help header and the TUI, so the two wordmarks cannot drift. `RowKind::Banner` keeps the splash as data, like every other width-dependent row: a resize re-lays it out rather than leaving it sized for the width it was committed at. A terminal too narrow for the wordmark gets the name as text instead of a clipped one, `hint_rows` wraps between hints (dropping a description only when a hint does not fit alone), and the invite shortens rather than wrapping. No information is lost: the sandbox/`--safe` line and the "type a message to start" invite moved into the splash, and the invite is dropped when `--task` already seeded the first message. * refactor(agent-tui): drop the header name chip, lead with the model The splash now names the app, so ` jan agent ` in the header said it a second time -- and spent 11 of the header's columns doing it, which is what pushed `ctx` and the status badge off a 60-column frame. The model leads instead, bold so the row keeps an anchor on the left; an unset model reads `no model` in red rather than opening the row with blanks. With the model permanently in the header, the splash's own `model` row was the duplicate, so it goes too. The `project` row stays despite the dock also carrying it: that copy is dim and easy to miss, and the tools act on that directory. The splash also resets `last_kind`, so a startup note (or the first message) gets a blank line off it instead of butting against its last row. * feat(agent-tui): give system lines their own gutter and severity Notes were dim text flush against the left margin, which is exactly how model prose renders -- "not signed in" read like something the model said. Every other transcript class already owns a two-column gutter (`› ` user, `│ ` tool, `┊ ` reasoning), so system lines get one too: `• `, coloured by severity, with the body keeping its emphasis. `Level` (Info/Warn/Error/Good) replaces a colour picked by hand at each site, and `system_marked` lets a category with an established glyph keep it, so the column never carries two markers -- the goal loop's lines now sit under `◎`, matching the header badge. Prose stays gutterless, which is what makes the rest readable as "not the model". Folded into one path: the turn's error line, the early-finish warning, the cancel line, the permission denial (which had hand-written its own `•`), the todo reminder (its `◈` was a second marker), and the `/help`, `/threads`, `/login` and `/goal` block headers. * feat(agent-tui): run the system gutter down the whole block A multi-line system block (`/help`, `/threads`, a goal status) marked only its first line, so its body floated unattributed under the header. The body rows now carry `┆`, giving the block one unbroken left edge -- distinct from the tool rows' `│` and the reasoning rows' `┊`, so three stacked blocks stay tellable apart. That needed system lines to become width-aware: as plain `Line`s, a row too long for the terminal was soft-wrapped by the paragraph, and the continuation landed in column 0 with no gutter at all -- exactly the unattributed text this is meant to fix, and it broke the left edge as well. `RowKind::System` keeps the source and wraps at the draw width like the other width-dependent rows, so a resize re-lays it out. `cont` says what a continuation puts in the column: an edge glyph repeats, a marker glyph gives way to blanks, since a second `•` would read as a second note. Wrapping goes through `wrap_spans_at_words`, a word-aware sibling of the code-block wrapper (which stays hard-splitting -- code has no spaces to break on). A token longer than the width is still split hard. Also drops a stale doc comment that had drifted onto `clear_selection`.
…8643) Hiding: the carve-out covered `.jan/agent` and only hard-denied it. `ls` still listed the directory before refusing to open it, and the thread store under it left the conversation's own transcripts greppable. Widen the prefix to all of `.jan` (`JAN_DIR`) and make hiding mean absent: `ls` omits the entry, `find`/`grep` keep filtering it during traversal, and path args and bash tokens hard-deny. Anything added under `.jan` later is covered without a new rule. Back it with the OS sandbox, so the spellings a token scan cannot see (`cd .jan`, `$(echo ...)`, a generated script) are unreachable rather than merely unscanned: `Policy::hide_root` mounts an empty tmpfs over `<root>/.jan` after the workspace bind on bubblewrap, and denies the subpath last (later rules win) on Seatbelt. AppContainer grants the workspace by an ACE and cannot carve a subpath back out without writing a deny ACE onto the user's directory, so there the scan stands alone. `HardDeny` now carries a reason: a hidden hit used to be reported as "see [tools] deny in agent.toml", sending the model to read a file that is itself hidden. /init: it submitted its canned onboarding prompt as an ordinary user turn, so the whole prompt body was rendered as a `>` row and journaled for replay. `submit_user_hidden` starts the turn without displaying or journaling it; the command's own note is what the user asked for. Hidden turns leave staged images alone rather than attaching them to a prompt the user did not write. Promote the command as well: it is the first thing a new project wants and was invisible. It joins the splash hints, and a project with no instructions gets a one-line invitation. Both the invitation and the command's own note reuse the rule the system prompt uses (`context::has_context_file`), so an ancestor's JAN.md counts as onboarded and stays quiet.
…un on headless hosts (#8646) * fix(agent-cli): recover from context overflow, and stop trusting a disproven context window Four defects left a CLI session stuck once it overflowed the model's context window, and made the numbers on screen describe something other than what was happening. Compaction never reached the session. `turn_cycle` compacted into a local `conversation_messages` but only published `MessagesUpdated` on its two return paths, so a run that never recovered (attempts exhausted, or a compaction that failed to shrink) threw the compacted conversation away with the task. The TUI kept the oversized history and every later turn re-overflowed by construction. It is now published as soon as it lands. The error path never compacted. `should_auto_compact` was consulted only in the `Done` arm, so an overflow went to `on_error`, which went idle and dequeued the next queued message straight back into the same oversized history. `on_error` now recognizes the overflow marker and queues a compaction plus a retry of the turn that failed, bounded to `MAX_OVERFLOW_RETRIES` per user turn so a model that overflows at any size still hands control back. Abandoned recoveries share `halt_turn` with the ordinary error path, so neither drifts. The summarizer request was larger than the one that overflowed. `summarize` replayed the entire conversation plus a prompt, which after an overflow cannot succeed: it errored and silently substituted FALLBACK_NOTE, deleting the dropped span instead of condensing it -- including for `/compact`. It now takes only the span being dropped, rendered to a text transcript (so a trailing tool call whose result is in the kept tail cannot break tool pairing) and clamped head-and-tail to a character budget. Nothing checked the context window against reality. `context_window` is a config guess with nothing tying it to the model in use, yet the header gauge and the subagent share divided by it unclamped, reading past 100% on any model with a larger window -- exactly when `should_auto_compact` also fired every turn. A prompt the provider accepted now disproves it: the session stops using it as a denominator, drops it from the gauge, hides the subagent share, stands down proactive compaction in favour of the reactive path, and says once how to set it. The share is clamped regardless. `estimate_token_count` also read arguments off the tool call instead of `function.arguments`, scoring every tool-heavy history as empty; it now counts arguments, names, tool-result ids, multimodal text parts and a per-message envelope. * feat(agent-cli): add --output-format json for machine-readable run results Add a --output-format flag to 'jan cli agent run' that replaces the streamed prose with a single JSON result object on stdout when the run finishes. Progress and diagnostics stay on stderr, so stdout pipes cleanly into jq. The envelope is folded from the same event stream the printer reads (no second source of truth), reports the partial answer on failure, classifies context_overflow vs upstream_error, and sums usage across turns and subagents. * fix(agent-cli): drop the libdbus link so the CLI starts on headless hosts keyring's `sync-secret-service` backend pulls dbus-secret-service -> dbus -> libdbus-sys, which links the system libdbus. That put a DT_NEEDED on libdbus-1.so.3 into every `jan` binary, and the loader resolves it before `main`, so a headless host without the D-Bus runtime failed at exec: jan: error while loading shared libraries: libdbus-1.so.3: cannot open shared object file: No such file or directory provider_secrets already degrades gracefully when the Secret Service is missing (KEYRING_DOWN latches and it falls back to the encrypted file), but none of that code ever got to run. Switch to `async-secret-service`, which reaches D-Bus through zbus and is pure Rust, so nothing is linked and an absent daemon is just an error the existing fallback handles. Pair it with `async-io` rather than `tokio`: zbus's `tokio` block_on drives a runtime of its own and panics ("Cannot start a runtime from within a runtime") when called from inside one, and load_provider_keys is sync but reached from async CLI paths via cli::providers. `async-io`'s block_on merely blocks the calling thread, as the old sync backend did. Covered by a regression test that fails with the `tokio` feature and passes with `async-io`. Verified: libdbus-1.so.3 is gone from the binary's NEEDED entries, and libdbus-sys is absent from both the `cli` and `test-tauri` graphs.
* fix(agent): avoid forcing unavailable tools * fix(agent): expose custom Ask responses * test(agent): align Ask result contract
The workflow landed on main (#8649), but `pull_request` resolves workflow files from the merge ref -- head merged into base. A dev-based branch merged into dev contains no rust-check.yml, so nothing was selected and the gate never fired. Putting it on dev is what makes it apply. Ports rust-check.yml (with the CLI config linting --all-targets) and scripts/stub-tauri-resources.sh, and drops rust-coverage.sh's inline copy of the stub logic in favour of the shared script.
…8650) Silence clippy lints that surfaced under --all-targets -D warnings and the newer (1.9x) toolchain, in code exercised only in test builds. - utils/math.rs: inline let_and_return, manual_clamp, use RangeInclusive contains() in tests, const-assert constant-invariant test - utils/string.rs: for-loop over char_indices, Option::is_some_and, !is_empty trail - utils/network.rs: strip_prefix for *. wildcard host check; drop needless borrows on Command .args() - utils/system.rs: strip_prefix for the Windows \\?\ device-path prefix - utils/crypto.rs: keep % == 0 (is_multiple_of would raise MSRV above the declared 1.82 floor), via targeted #[allow] - agent/loop.rs: collapse collapsible_match test arm into a match guard - agent/global_config.rs: contains_key instead of get().is_none() in test - agent-tools/tools/gate.rs: #[allow] join_absolute_paths to keep the absolute-path-escape semantics of the test (clippy's suggested fix would have gutted its intent)
* fix(llamacpp): trim shifted chat context * test(llamacpp): cover context-shift request trimming
) * feat(cli): send an anonymous usage ping from the headless jan CLI Reuses the desktop's HMAC-signed update-check endpoint purely as a usage counter: once per 24h, fires a signed request with the CLI version, OS/arch, and a persisted anonymous install id. Shares the existing update check's JAN_CLI_NO_UPDATE_CHECK opt-out and its silent-failure behavior. * fix(cli): send Jan-Agent in the usage ping's User-Agent, not Jan The CLI ping reused the desktop's exact User-Agent format and shares its version number, so the two were server-side indistinguishable. Naming the CLI client "Jan-Agent" lets the analytics backend split them apart. * fix(cli): satisfy clippy let-unit-value in telemetry opt-out test
* feat(agent): add built-in Jan self-knowledge skill * fix(agent): refine Jan onboarding skill * docs(agent): map Jan Agent storage * docs(agent): use ASCII storage navigation * docs(agent): document Jan file locations * fix(agent): correct Jan instruction guide
Introduce a progressive-disclosure memory catalog: curated notes are advertised by name plus a one-line summary in the system prompt, and the model loads each full note on demand with memory_read. - agent-tools/memory: add catalog() and describe() helpers (auto-tested) - context: inject an '# Available Memories' block via load_memory_catalog - default_skill: point at memory_read instead of automatic recall - remove the vector-db backed retrieve/index flow from loop.rs and the old core/agent/memory.rs module
…t.rs (#8664) Remove the per-turn "Today's date is" line prepended in the orchestration loop; the runtime environment block already emits the date, so it appeared twice on every project run. Prompts now live only in context.rs: move the eager-todo and todo-upkeep system-prompt addenda out of loop.rs into context.rs and reference them from there.
* feat(agent): reword default identity to Jan agent harness * feat(agent-cli): recolor thinking badge yellow and bold text orange * perf(agent-cli): lay out only the visible transcript rows draw cloned every row's lines and let the body Paragraph word-wrap the whole transcript twice per frame (once for line_count, once to render), so per-frame cost tracked session history rather than the viewport: at width 100, 3.2ms/frame at 100 rows, 58ms at 2000, 236ms at 8000 -- past the 50ms tick, which starved input and made long sessions crawl. Memoize each row's wrapped height alongside its lines, sum those to get the scroll total, then materialize and wrap only the segments overlapping the visible window. Flat at ~0.7-0.9ms regardless of length. row_index is now one entry per visible body row in wrapped coordinates, which also fixes click-to-expand drifting whenever a row wrapped. * fix(agent-cli): drop seconds from the TUI clock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )