[go: up one dir, main page]

Simon Willison’s Weblog

Subscribe
Atom feed

Elsewhere

Filters: Sorted by date

Comment My comment on Devtools must be open source (exe.dev) — Hacker News

One of the arguments for open source software for end-users has always been the freedom to examine and modify how that software works.

The reality for most people - even expert programmers - has been that the freedom is more about being able to lean on other people to do that. Most people can't justify the time commitment needed to read and then modify the code for tools they use very often.

I think LLMs have changed that equation in a way that makes the original dream much more feasible.

Several times a day I'll prompt regular Claude chat to "Clone x/y from GitHub and tell me how Z works".

Getting software to compile in order to start hacking on it used to be enough friction that I often wouldn't bother. Now I treat that as a zero time investment challenge: tell Codex or Claude Code to checkout and build X and then come back ten minutes later and see how it got on.

I'm not habitually modifying the software I use yet, but I can see a path to that which didn't exist a year or so ago.

# 3rd August 2026, 3:30 pm / hacker-news, open-source, ai, generative-ai, llms, ai-assisted-programming

I'm trying to get braver at releasing 1.0 versions. This little library is a year and a half old now - I've applied some sensible and non-disruptive fixes and shipped the big 1.0 for it.

Here's an example of what it can do, lifted from the README:

{
  "foo": {
    "bar": {
      "string": "This is a string with foxes in it",
      "nested": {
        "more": ["Here is a string", "another with foxes in it too"]
      }
    }
  }
}

Combine that with a replacements object:

{"1": "with foxes in it"}

And condense_json(input_json, replacements) produces the following:

{
  "foo": {
    "bar": {
      "string": {"$r": ["This is a string ", {"$": "1"}]},
      "nested": {
        "more": ["Here is a string", {"$r": ["another ", {"$": "1"}, " too"]}]
      }
    }
  }
}

It scans for strings or substrings that are present in that replacements object and replaces those with a special {"$r": ...} syntax in the output.

You can reverse the effect with uncondense_json(condensed, replacements).

The idea is to make it easier to store JSON that includes duplicated data from other related structures. I use it to save space in the SQLite logs generated by LLM - see PR #1586 for the latest iteration of that.

Changes that improve Datasette Apps when created and edited using Datasette Agent:

  • New app_debug() tool allowing agent to open an app (invisibly) and test it using JavaScript. #33
  • New app_list() tool for listing apps the user has permission to edit, so the agent can edit them. #36

The app_debug() tool is pretty neat: it works by displaying the app in a opacity: 0 iframe with pointer-events: none (so it can't be seen or interacted with) and then executing agent-provided JavaScript inside that sandboxed iframe. This means the agent can smoke test that the app is working and even do things like measure the dimensions of different elements.

This uses the new context.browser_task() mechanism added in datasette-agent 0.4a0.

I wanted to create a new Slack emoji, and their tool recommends a square that's 128x128 and has a transparent background... so I had Fable build me this simple image editor against those requirements.

  • New await context.browser_task() mechanism allowing agent tools to run code directly in the user's browser. #33

This is an exciting new capability: it makes it easy for Datasette Agent plugins to provide tools that execute custom JavaScript in the user's browser.

I used this to add a debug loop to Datasette Apps in datasette-apps 0.2a0.

Hot on the heels of RC1, this fixes a dependency issue and also adds two neat new features:

  • The default model for users who have not set their own default is now GPT-5.6 Luna. It was previously GPT-4o mini. Luna is a much better and more recent model, albeit slightly more expensive - $0.20 per million input tokens and $1.20 per million output tokens, compared to $0.15/$0.60 for 4o mini. You can switch back to 4o mini using llm models default gpt-4o-mini, or switch to GPT-5 nano, an even cheaper default model ($0.05/$0.40), using llm models default gpt-5-nano. #1576
  • New llm openai endpoint command for running prompts, chats and model listings against arbitrary OpenAI-compatible endpoints without first configuring a model. These calls are not logged. #1565

The llm openai endpoint command is really cool. I got frustrated at the lack of an obvious CLI tool for trying out prompts against arbitrary OpenAI Chat Completions imitation endpoints, so I decided to add that to LLM itself.

You don't even have to install LLM to use this. Here's a uvx one-liner for running a prompt - with tools - against an LM Studio local model:

uvx --pre llm openai endpoint http://127.0.0.1:1234/v1 \
  T llm_version -T llm_time --td \
  -m google/gemma-4-31b 'what is the current LLM version? And the time?'

Output here.

A key goal of the new content-addressable logs in LLM 0.32rc1 was being able to support OpenAI Chat Completion style requests where each incoming message extends the previous conversation, like this:

curl http://localhost:8002/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3.5-4b",
    "messages": [
      {"role": "user", "content": "Capital of France?"},
      {"role": "assistant", "content": "Paris."},
      {"role": "user", "content": "Germany?"}
    ]
  }'

Here the conversation state is tracked by the client, so each of these requests gets longer and longer. The new schema design in LLM is designed to de-duplicate these using hashes of the individual message parts.

To test that out, I built this plugin:

uv tool install llm --pre
llm install llm-chat-completions-server
llm chat-completions-server -p 9001

Running this starts a localhost server on port 9001 that exposes your full collection of LLM models (from any plugins you have installed) using a ChatGPT Completions compatible endpoint.

GPT-5.6 Sol wrote the whole thing - it turns out it knows the OpenAI Chat Completions API shape really well.

This RC for LLM 0.32 finishes the work that started in LLM 0.32a0 - it adds a new schema design that does a much better job of capturing the details of the prompts and responses returned by the latest model families.

The most important change is the use of content-addressable hash IDs for stored messages. This allows de-duplication in the database, and means that LLM can now represent trees of messages for forked conversations.

Since it involves a significant schema change - new tables only, and old data should not be affected at all - it's worth running a backup of your existing logs.db before upgrading to the RC:

llm logs backup logs-backup.db

The RC also adds support for gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna.

Sighting 8:32 PM – 8:57 PM — California Brown Pelican, Caspian Tern, in Monterey Bay National Marine Sanctuary, CA, US, CA
California Brown Pelican
California Brown Pelican
Caspian Tern
Caspian Tern
None

Connecting a custom MCP server to Claude and ChatGPT's standard chat interfaces is possible, but can take quite a few steps.

Sighting 8:19 PM — California Brown Pelican, in Monterey Bay National Marine Sanctuary, CA, US, CA
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican

I back-ported a fix for table.delete_where() that shipped in version 4.

Sighting 8:36 PM — California Brown Pelican, in Monterey Bay National Marine Sanctuary, CA, US, CA
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican
Sighting 1:15 PM — Sea Otter, in Monterey Bay Area, CA, US
Sea Otter
Sea Otter
Sighting 12:51 PM — California Sea Lion, in San Francisco County, US, CA
California Sea Lion
California Sea Lion
California Sea Lion
California Sea Lion

We took some visiting family to Pier 39 to see the sea lions. They're somehow always even more fun than I remember them being last time.

Sighting 6:23 PM – 6:28 PM — Elegant Tern, California Brown Pelican, in Monterey Bay National Marine Sanctuary, CA, US, CA
Elegant Tern
Elegant Tern
Elegant Tern
Elegant Tern
California Brown Pelican
California Brown Pelican

Julia Evan's, in Learning a few things about running SQLite:

Maybe one day I’ll learn to read a query plan.

Big same.... which inspired me to have Fable build this interactive explain tool, which runs SQLite in Python in Pyodide in Web Assembly in the browser and adds a layer of explanation to the results of both EXPLAIN and EXPLAIN QUERY PLAN.

Approach with caution, since I don't know enough about SQLite query plans to verify the results myself, but it seems cromulent enough to me.

I got frustrated reading yet another article that was crammed with the clichés of LLM-generated writing - "no fluff, no filler, no jargon" type stuff - so I had Fable 5 vibe code up this app for highlighting ten common patterns that show up in that sort of writing.

Screenshot of a text-analysis web tool. Top summary row: "2 matches", "1 flagged sentence", "0 chain items". Below, a collapsed "▶ Patterns · all 11 on" panel, then a URL input reading "https://example.com/article — fetched via r.jina.ai" with a "Load URL" button. A text area contains "That loss is real and it's worth naming". Below are "Load example" and "Clear" buttons and a checked checkbox "Show just the highlights". A "Highlighted text" section shows "That loss is real and it's worth naming" with "That loss" in pale yellow (flagged sentence) and "is real and" plus "'s worth naming" in darker yellow (pattern match). Legend: "flagged sentence", "pattern match", "3 chain item count". "Matches" section: 1. "is real and" — "Is real … and / not"; 2. "'s worth naming" — "Worth naming".

Sighting 7:59 PM – 8:08 PM — Pacific Harbor Seal, California Brown Pelican, in Monterey Bay National Marine Sanctuary, CA, US, CA
Pacific Harbor Seal
Pacific Harbor Seal
California Brown Pelican
California Brown Pelican

After building the Mermaid to ASCII tool based on Grok Build's Rust code I learned that there's an older, more fully-featured Go library called AlexanderGrooff/mermaid-ascii that implements a similar pattern, so I had Claude Fable 5 compile that one to WebAssembly as well so I could compare the two.

This one includes support for colors!

Screenshot of a Mermaid diagram editor web app. A row of tab buttons reads: Flowchart, Multiple links, Subgraphs, Multi-line labels, Colors (selected, highlighted blue), Sequence, Alt fragment, Loop + note, Parallel. Below is a text input area containing: "graph LR / Build:::good --> Test:::good / Test --> Deploy:::warn / Deploy --> Rollback:::bad / classDef good color:#3fb950 / classDef warn color:#e3b341 / classDef bad color:#ff7b72". A control row shows an unchecked "ASCII only" checkbox, "Padding X: 5", "Padding Y: 5", "Box padding: 1", and buttons "Copy as text" and "Copy link to this diagram". At the bottom on a black background is the rendered left-to-right flowchart with four connected boxes: "Build" (green text), "Test" (green text), "Deploy" (yellow text), "Rollback" (red text), each linked by arrows.

Sighting 8:14 PM – 8:28 PM — Great Blue Heron, California Brown Pelican, Willet, in Monterey Bay National Marine Sanctuary, CA, US, CA
Great Blue Heron
Great Blue Heron
Great Blue Heron
Great Blue Heron
California Brown Pelican
California Brown Pelican
Willet
Willet

While exploring the codebase for the newly open-sourced Grok CLI coding agent I came across xai-grok-markdown/src/mermaid.rs, a "self-contained terminal renderer for Mermaid diagrams" written in Rust.

I figured it would be fun to try that out in a browser via WebAssembly. Here's the prompt I ran in Claude Code for web (Fable 5), and this is what the resulting tool looks like:

Screenshot of a Mermaid diagram editor showing source code and rendered flowchart. The code reads: graph TD Start[Request received] --> Auth{Authenticated?} Auth -->|yes| Rate{Rate limit OK?} Auth -->|no| R401[401 Unauthorized] Rate -->|yes| H(Handle request) Rate -->|no| R429[429 Too Many Requests] H -.-> Log[Audit log] H ==> Resp[200 OK]. Below the code are controls labeled Max width: Fit output panel, Copy as text, and Copy link to this diagram. The rendered flowchart on a dark background flows top-down: Request received leads to Authenticated?, which branches yes to Rate limit OK? and no to 401 Unauthorized. Rate limit OK? branches yes to Handle request and no to 429 Too Many Requests. Handle request connects with a dotted arrow to Audit log and a thick arrow to 200 OK.

Sighting 8:37 PM – 8:51 PM — Double-crested Cormorant, California Brown Pelican, Great Blue Heron, in Monterey Bay National Marine Sanctuary, CA, US, CA
Double-crested Cormorant
Double-crested Cormorant
California Brown Pelican
California Brown Pelican
Great Blue Heron
Great Blue Heron
Double-crested Cormorant
Double-crested Cormorant

A minor release. Performance and documentation improvements to the permissions system, plus I reverted a cosmetic API change which caused almost every existing plugin test suite to break.

None

I finally found a cache-friendly recipe for using uvx tool-name in GitHub Actions workflows that I like.

The trick is setting a UV_EXCLUDE_NEWER: "2026-07-12" environment variable at the start of the workflow and then using that as part of the GitHub Actions cache key. This means any uvx tool-name commands will resolve to the most recent version as-of that date, and you can bust the cache and upgrade the tools by bumping the date in the future.

My goal here is to use Python tools in GitHub Actions without every run of the workflow hitting PyPI to download a fresh copy of the tool and its dependencies.

Update: Here's an existing issue against the astral-sh/setup-uv repository requesting that they switch the default to cache rather than purge wheels from PyPI.

Sighting 7:03 PM – 7:04 PM — Brewer's Blackbird, California Brown Pelican, in Monterey Bay National Marine Sanctuary, CA, US, CA
Brewer's Blackbird
Brewer's Blackbird
California Brown Pelican
California Brown Pelican
California Brown Pelican
California Brown Pelican

Some minor improvements, mainly around command option consistency and making the server: mechanism used by both shot-scraper video and shot-scraper multi work if the server takes longer than a second to start serving traffic.

  • server: processes used by shot-scraper multi and shot-scraper video now wait up to 30 seconds for the target URL to accept connections, polling for port availability and replacing the previous fixed one-second delay. #197
  • The shot-scraper, pdf, html, accessibility and har commands now have a --js-file option for loading JavaScript from a local file, standard input or gh:username/script, as an alternative to --javascriptwhich accepts the string of JavaScript directly as an argument. #192
  • shot-scraper multi supports the equivalent js_file: YAML key.
  • The shot-scraper javascript and shot-scraper html commands now have a --timeout option for consistency with other commands. #118

Mainly a fix for an edge case that regular Claude chat spotted while experimenting with the 4.1 release to answer a question about ON DELETE.

  • table.transform() now raises a TransactionError if called while a transaction is open with PRAGMA foreign_keys enabled and the table is referenced by foreign keys with destructive ON DELETE actions - CASCADESET NULL or SET DEFAULT. The pragma cannot be changed inside a transaction, so previously dropping the old table as part of the transform could fire those actions and silently delete or modify referencing rows. See Foreign keys and transactions for details and workarounds. (#794)
  • The CLI and Python API documentation now cross-reference each other: CLI sections link to the equivalent Python API functionality and Python API sections link back to the corresponding CLI command. (#791)