Commit Graph
369 Commits
Author SHA1 Message Date
Adrià Arrufat 290e4f81d9 agent: simplify REPL command dispatch and provider bookkeeping
- `available_providers` holds the static enum tag names; drop the dupe
  loop, its errdefer and the per-string frees.
- `reconcileModel` returns `error.ModelNotAvailable` directly instead of
  a `use`/`abort` union the caller only mapped to that same error.
- `runCommand`/`printCommandResult` take `Command.ToolCall`; the caller
  already has it, so the unreachable "no tool mapping" branches go.
- `handleSave` reuses `rememberSavePath`, which now propagates its
  allocation failure so a first save under OOM warns instead of
  unwrapping a null `save_path`.
- `SlashCommand.all_names` is a comptime `++` of the three name lists.
- `buildUserMessageParts` reads the attachment once for both text and
  image, and prepends the text part instead of copying the list.
- `printSeverity`/`formatBulletLine` use `allocPrint` and the shared
  `emitStderr`; drop an unreachable `ends_ws` check in `renderMetaHint`.
2026-09-18 00:45:55 +02:00
Halil Durak 901e5eaf4d Agent: add lightpanda run -
Arbitrary JS code can be passed directly through stdin thanks to this.
2026-09-15 16:21:01 +03:00
Karl Seguin 2e6999f20b chore: make declarations private if they don't need to be public
This change is 99%  s/pub//   + a handful of dead code removal.
2026-09-10 14:42:09 +08:00
Adrià Arrufat d8e804cdb4 Merge branch 'main' into repl-markdown-render
# Conflicts:
#	src/agent/Agent.zig
2026-08-31 16:48:43 +02:00
Adrià Arrufat f1bdd0019f Merge branch 'main' into tool-session 2026-08-28 15:14:56 +02:00
Adrià Arrufat 2899bfef4a agent: route REPL output per tool and align page tables of any size
Every data command went through printAssistant, so /markdown was styled by
accident while /tree, /getUrl and /evaluate strings had `*`/`_` eaten as
emphasis, and /markdown lost table alignment past Stream's 16 KiB buffer.

- printData renders only markdown as markdown; JSON is re-indented and the
  rest prints verbatim.
- render iterates the source directly and aligns tables from the slice;
  Fence holds the fence state shared with Stream.
- Links are OSC 8 hyperlinks around the label only, with the label
  inline-rendered; `![alt](url)` drops the `!`; escapes follow CommonMark
  so what the page dumper escapes is unescaped.
2026-08-28 14:38:27 +02:00
Adrià Arrufat d10029fb19 Merge branch 'main' into screenshot-tool 2026-08-27 21:13:36 +02:00
Adrià Arrufat 0865f4e337 agent: fold the browser quartet onto lp.ToolSession
Agent held the same Browser/Session/Notification/registry fields and
teardown order as mcp/Server.zig, plus its own "enableConsoleCapture
after every newSession" for /reset. ToolSession.restartSession() owns
that now; init is written in terms of it.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
2026-08-27 16:40:53 +02:00
Adrià Arrufat 45744407af screenshot: fit() owns the inline sizing; prune() owns image expiry
The renderer knows that layout reflows to the width and that height 0
means the whole content, so the bounding rule moves next to measure()
and only measures when the strip isn't already fixed: the default
viewport shot no longer pays a second layout pass. The limits are
constants spliced into the tool description, not a caller-tunable that
nothing tuned. expireImages runs inside prune so there is one end-of-
turn hook, and re-homes a stripped result instead of casting away const.
2026-08-27 15:03:19 +02:00
Adrià Arrufat c693979468 agent: bound what a screenshot costs the model
An inline screenshot is re-sent on every request for as long as it sits
in history, and a full-page render could reach 1920x16384. Inline images
are now rendered at most 1280 wide and 4096 tall (measured after the
reflow, so short pages aren't padded), files keep full size; the
conversation keeps only the newest two images, older tool results keep
their text with a note; the MCP transport releases its buffer after a
large response.
2026-08-27 15:03:19 +02:00
Adrià Arrufat cff32f6c04 agent: let the model see a REPL /screenshot; fold the tool-result adapters
The slash path's result has two consumers: the terminal, which can't
show an image, and the conversation, which can. Opt in when a model is
attached and forward the image through the same adapter the model-driven
path uses, and stop mapping a failed adapter to a text-only success.
MCP's ImageContent and CallToolResult take their payload type like
TextContent does; resolveScope reuses resolveTarget; needsLocator folds
into replayRequires.
2026-08-27 15:03:19 +02:00
Adrià Arrufat e6203ff1fe agent: hand the model inline screenshots
zenai tool results now carry image parts (lightpanda-io/zenai#12, #13),
so the model-driven tool path opts into inline images: a screenshot
without `path` reaches the model as text plus the PNG, on every backend.
The slash-command path still needs `path`, its result goes to the
terminal.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 193f15558c tools: declare image support at the call site, not in each consumer
Callers pass CallOpts.inline_image; execScreenshot rejects a path-less
call before navigating or rendering, which removes the per-consumer
guards and covers the model-driven tool path that had none. MCP image
content is a protocol type, the screenshot recording rule joins the
recorder's replayRequires predicate, and the viewport-to-Opts mapping,
node-scope ladder and save-path helpers are shared instead of copied.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 56c6a0a7ab tools: add a screenshot tool
The PNG renderer was reachable from CDP and fetch --dump png only. The
tool renders the page or one node; with `path` it writes the file and
returns its location (agent, PandaScript, MCP), without it MCP returns
the image inline as base64 content. ToolResult carries the prepared
image so the transport streams it; the agent and script runtime reject
the inline form since their tool results are text. An inline screenshot
is not recorded, as it has no replayable form.
2026-08-27 15:03:19 +02:00
Adrià Arrufat ed99a5b79f agent: run the Terminal tests
Terminal.zig references md_term and prompt_assist from a test block, but
nothing referenced Terminal, so those 30-odd tests never ran. Adding it
to Agent.zig's discovery block surfaced one rotted test sink in
js_highlight.zig still using ArrayList.writer(), ported to
Io.Writer.Allocating.
2026-08-27 12:03:23 +02:00
Adrià Arrufat c3b8f43f5f search: resolve the engine key once, retries off in one place
`engineKey` (on `lp.environ().getPosix`) replaces the env-var wrapper and
the three hand-spelled set/keyless/missing decisions in the cascade, the
explicit path, and the REPL query. `.auto`'s "always a rung" invariant is
now a comptime assert instead of an unreachable sentinel. HTTP retries are
disabled in `apiSearch` for every path — the cascade or the model is the
retry, and the tool's `timeout` argument bounds one attempt — rather than
threaded through as a parameter the table no longer set. The cascade
sentence is one shared constant for the tool description and the REPL
help, and search gets its own default timeout instead of borrowing the
navigation one.
2026-08-25 09:43:06 +02:00
Adrià Arrufat 42c3cbf20b search: default to Keenable's keyless endpoint, drop the DuckDuckGo scrape
The DuckDuckGo HTML endpoint the search tool fell back to is disallowed by
its robots.txt, so it is removed: the `duckduckgo` engine, the scrape via
`performGoto`, and the tool's browser-side dependency (`execSearch` no
longer needs a session). In its place `.auto` is a single walk over the
API engines in table order — each when its key is set — with Keenable
last, keyed if `KEENABLE_API_KEY` is set and its public endpoint
otherwise. Search therefore always has a rung with zero configuration,
and a set key is never shadowed by a keyless retry.

Every search now goes through zenai's `std.http.Client`, which had no
timeout at all; a stalled response would have blocked the MCP server's
shared browser thread for every session. zenai now bounds each attempt
(lightpanda-io/zenai#10, pinned here), and the tool's `timeout` argument
drives it, defaulting to 10 s. Retries are a property of the call path:
off in the cascade, where the next rung is the retry, and the engine
default for an explicit engine.

Cleanups from the #3252 review: `keyless` is derived from the client's
`api_key` type instead of a hand-set table flag; `engineIndex` no longer
depends on enum declaration order; one `searchKeyStatus` query replaces
three pub helpers for the REPL; the engine order and env-var list in the
tool description and `/searchEngine` help are generated from the table;
an empty result title renders as its URL instead of `****`.
2026-08-25 09:35:26 +02:00
Ilya Bogin 6cbe30b088 search: add Keenable engine with a keyless rung above the DDG scrape
Adds keenable to the search tool's engines (KEENABLE_API_KEY, tried
after brave/tavily/exa in .auto). Unlike the other engines it also
answers without any key: its client routes an empty key to the public
endpoint (rate-limited per client IP), so .auto now tries that as the
last rung before the DuckDuckGo scrape, and an explicit
/searchEngine keenable works keyless instead of erroring. Engine table
entries gain init_options so the Keenable client can carry the
lightpanda attribution header.

Needs the zenai Keenable search client (lightpanda-io/zenai PR #9); the
zon pin points at that branch until it lands.
2026-08-23 15:42:06 +03:00
Adrià Arrufat d76ead300a browser: add exa search engine support 2026-08-18 21:10:45 +02:00
Adrià Arrufat e69aedea14 tools: treat backendNodeId 0 as omitted, add recovery hints to errors
Some models (gpt-5.x via codex) zero-fill optional tool params instead
of omitting them, so every tree/markdown/html call carried
backendNodeId: 0, failed with NodeNotFound, and got retried blind --
one 'go to hacker news' burned 19 tool calls and 18 full page loads.

Registry ids start at 1, so 0 can never name a real node. Make
0-means-omitted part of the tool contract: state it in the schema
descriptions and normalize it in parseValue. Required ids
(nodeDetails) are untouched.

In-band guidance alone was not enough -- the model re-emitted the
identical call even when the error spelled out the fix -- but keep it
for genuinely stale ids: NodeNotFound and FrameNotLoaded now carry an
actionable message in the agent, slash-command, and MCP paths instead
of a bare error name.
2026-08-06 14:20:34 +02:00
Adrià Arrufat 97e26ae86e agent: add keep/login/logout menu for subscription providers 2026-07-29 10:07:52 +02:00
Adrià Arrufat 27b37e6c93 auth: simplify token refresh and json store helpers 2026-07-29 09:55:49 +02:00
Adrià Arrufat 41e365b3f4 agent: move session banner printing to Terminal 2026-07-29 09:21:02 +02:00
Adrià Arrufat 8b6f7fe064 agent: simplify credential and key ownership handling 2026-07-28 13:05:46 +02:00
Adrià Arrufat a53fbcc6eb agent: introduce KeyOwnership union for credentials
Consolidates owned_key, session, and unowned env key management into a
tagged union KeyOwnership to simplify resource lifetime handling.
2026-07-28 12:44:37 +02:00
Adrià Arrufat 00cdc3ba8d agent: update zenai and switch auth to interruptible HTTP 2026-07-28 11:17:08 +02:00
Adrià Arrufat 01a7b091db agent: map models_dev_id and filter tool-call models 2026-07-28 10:56:24 +02:00
Adrià Arrufat 819aee3c07 agent: support login cancellation and provider completion` 2026-07-28 10:28:18 +02:00
Adrià Arrufat d7cc090ca1 Merge branch 'main' into codex 2026-07-28 10:11:31 +02:00
Adrià Arrufat a2e8ce8f50 agent: reduce tool output cap to 64KB except for extract 2026-07-28 09:17:34 +02:00
Adrià Arrufat e99a5ab99b agent: pivot subscription auth from Claude Code to Codex
Replace the Anthropic Claude-Code-import flow with OpenAI Codex (ChatGPT
subscription) OAuth: device-code login, a real token store (auth.json,
0600) with refresh-grant, and JWT ChatGPT-Account-Id extraction. Codex is
now a distinct zenai provider, so the bearer-mode credential collapses to
provider==.codex; account_id threads to the client via InitOptions.

Unit-tested (build green): JWT account-id extraction, token store
round-trip, OAuth request-body encoding, token-response parsing. The
device-login / refresh network path is unverified pending a subscription.

Pins zenai to the codex branch (6967324).
2026-07-27 11:24:01 +02:00
Adrià Arrufat 41cd1cd734 agent: support subscription auth via Claude Code 2026-07-23 14:56:41 +02:00
Adrià Arrufat bae9388f1c Merge branch 'main' into brave-search 2026-07-22 16:27:35 +02:00
Adrià Arrufat 99262190da feat(tools): add Brave search + /searchEngine metacommand
Add Brave as a second API-backed web search engine: the search tool's
auto mode tries Brave then Tavily (keyed on BRAVE_API_KEY/TAVILY_API_KEY)
before the DuckDuckGo scrape. A new /searchEngine REPL metacommand pins
the engine explicitly (auto|tavily|brave|duckduckgo) — an explicit
engine surfaces its failures instead of silently degrading — and is
persisted to .lp-agent.zon like the other REPL settings.

Bumps zenai to pick up the zenai.search.brave client.
2026-07-22 13:44:51 +02:00
Karl Seguin 802862120f update zenai dep (zig 0.16) 2026-07-22 13:26:06 +08:00
Karl Seguin 8e42d63c1c zig: Zig 0.16
Built against https://github.com/lightpanda-io/zig-v8-fork/tree/zig-0.16 but
it doesn't require a new v8 build.

Built against https://github.com/lightpanda-io/boringssl-zig/tree/zig-0.16
since the current fork we point to isn't updated.

A global std.Io instance, lp.io. Way easier this way and requires 0 changes to
our libcurl integration / event loop.

Network code uses a new layer that does what Zig 0.15's posix package used to
do. Again, quicker migration that way. But, as long as we have the global IO,
and given the half-baked nature of networking in std.Io 0.16, this just makes
sense. Things can be migrated as needed.

The std.time.* -> std.Io.Timestamp/Clock/Duration resulted in _a lot_ of
changes. ArrayList = .{} -> ArrayList -> .empty also resulted in a lot of
changes, but that's obviously superficial. As is the trimLeft/trimRight ->
trimStart/trimEnd rename.

Locking adopt the `Uncancelable` variants, e.g. mutex.lockUncancelable() to
preserve the error-free signature (and, because cancellation would be something
we'd have to put more thought into).

std.json.ObjectMap is now unmanaged, so the allocator had to be passed along.
However, there's still a deprecated managed variant of MemoryPool, so I switched
to it (we can do a small follow up PR to move to the unmanaged after).

I tried use_llvm = false, but it locks my computer, consuming RAM until MacOS
gives me a popup I've never seen before, begging me to start killing processes.

Agent and the networking stuff saw the most significant changes.
2026-07-22 13:26:03 +08:00
Adrià Arrufat f462223277 agent: tighten the seams the terminal split left behind
- attachCompleter now takes the CompletionSource: Agent no longer
  reaches through terminal.assist to configure what it just attached,
  and attach/configure can't be sequenced wrong.
- history_paths flows through one channel: attach() loads the initial
  history from the same State field modeCallback swaps from, instead of
  setupRepl receiving a second copy that had to agree.
- all_names + closestCommand move to SlashCommand.zig, the command
  registry - prompt_assist consumes it like everyone else and
  Terminal's closestCommand re-export disappears.
- picker treats a tty that refuses raw mode as NotInteractive and
  degrades to the line prompt instead of leaking termios errnos to
  callers that can only blanket-catch.
- kitty keyboard flags become named ansi.zig constants shared by
  Terminal.readLine and picker's RawTerminal.
- skipWhitespace was std.mem.indexOfNonePos; single-use style consts
  inline into the style table; drop the dead CompletionSource default
  and doc-comment splice in Spinner.
2026-07-15 12:16:53 +02:00
Adrià Arrufat 673b16528f agent: extract prompt assistance into prompt_assist.zig
Completion, ghost hints, and prompt highlighting (~640 lines) were
cohabiting with Terminal's readline lifecycle and output printing. They
now live in prompt_assist.zig, which owns the isocline callbacks, the
ps-* style palette, and the slash-command name table. The C callbacks
read a small prompt_assist.State (js_mode, completion_source,
history_paths) embedded in Terminal, replacing the three loose fields.
Pure code motion otherwise; the valueAt/renderSchemaHint tests move
with their code and stay in the suite via Terminal's test hook.
2026-07-15 11:50:44 +02:00
Adrià Arrufat f48498073d agent: extract the choice picker into picker.zig
The numbered/interactive picker (ChoiceState, RawTerminal, render loop)
was ~215 self-contained lines inside Terminal.zig. It runs before - or
without - the isocline REPL (provider selection during setup), so
settings.zig no longer imports the isocline-configured Terminal at all.
Pure code motion; the ChoiceState tests move with it and stay in the
suite via Agent.zig's test hook.
2026-07-15 11:40:55 +02:00
Adrià Arrufat 4307509094 agent: improve REPL syntax highlighting and terminal colors
- Add highlighting for JS functions, methods, types, and template
  interpolations.
- Introduce teal color and adjust cyan to bright cyan.
- Import `ansi.zig` directly instead of through `Terminal`.
- Add PandaScript semantics note to the agent's system prompt.
- Refactor markdown table rendering and streaming.
2026-07-15 11:02:46 +02:00
Adrià Arrufat d0ce9a02a8 terminal: stream and render markdown incrementally
Introduces an incremental markdown renderer (`md_term.Stream`) to
format streamed assistant responses in real time. Buffers lines,
maintains code fence state across chunks, and temporarily withholds
tables to render them with proper column alignment once complete.
2026-07-14 22:51:43 +02:00
Adrià Arrufat 8c0ec3d73a Merge branch 'main' into pandascript-skill 2026-07-14 12:06:05 +02:00
Adrià Arrufat 3e801fb83b script: generate the PandaScript skill from the tool schemas
The /save script documentation lived as a hand-maintained string in
Agent.zig whose primitives table drifted whenever a tool changed. Move
it to src/script/skill.zig and render the reference (signatures, option
lists, enums, defaults, per-parameter descriptions) from Schema.all()
at first use, keeping the curated per-tool notes behind an exhaustive
switch on Tool so a new or renamed tool is a compile error until its
doc entry exists.

The rendered skill is shared by three consumers:
- the /save and revision system prompts (built lazily in Agent.zig)
- a new mcp://skill/pandascript resource
- `zig build skills`, which writes zig-out/skills/<name>/SKILL.md with
  Claude Code frontmatter via a registry-based generator exe, so
  future Lightpanda skills are one registry entry each

Schema.FieldEntry now retains per-parameter schema descriptions, which
previously existed only in the raw JSON.
2026-07-13 10:54:13 +02:00
Adrià Arrufat bdb122cdce Merge branch 'main' into agent-streaming 2026-07-12 22:56:54 +02:00
Adrià Arrufat fa2dc02721 fix(agent): reset stream_active at turn start
`stream_active` relied on every runTools exit path balancing pause with
`endStreamedText`. If one is ever missed, a stale-true flag skips the
spinner pause on the next turn, interleaving frames with streamed text.
Reset it alongside `streamed_text` so the invariant is local.
2026-07-12 22:10:29 +02:00
Adrià Arrufat 2e8e0d1a6f docs(agent): note why the streamed_text guard is turn-wide safe
Document the invariant that lets a single per-turn flag suppress the
buffered reprint without risking a dropped final answer.
2026-07-12 22:10:11 +02:00
Adrià Arrufat a64ced0ccc fix(agent): stream assistant text only in the REPL
Streaming defaulted on for every mode, so one-shot `--task` and script
runs interleaved intermediate assistant commentary into stdout, which
wrappers treat as the answer. Gate the stream hook on `Terminal.isRepl`
so non-interactive modes keep the buffered final answer.
2026-07-12 22:09:48 +02:00
Adrià Arrufat 5abae593d8 fix(agent): keep streaming off for suppressed turns
Streaming wrote assistant deltas straight to stdout regardless of
`suppress_answer`, so `--save`/capture turns (which set it to hide the
answer) leaked model text past the `runTurn` guard. Pass a null stream
hook when the turn's answer is suppressed.
2026-07-12 22:09:25 +02:00
Karl Seguin d43d11a641 Merge pull request #2912 from lightpanda-io/agent-repl-fixes
agent: multi-line slash commands and --save .js extension
2026-07-11 07:26:29 +08:00
Adrià Arrufat 049c5f08f2 feat(agent): add assistant text streaming 2026-07-11 00:50:01 +02:00