Compare commits
22
Commits
v0.2.0
...
v1.0.0-beta.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c2b0d045b
|
||
|
|
5c41da8b4d
|
||
|
|
ab0c1434c4
|
||
|
|
b82d4c688a
|
||
|
|
7f54d18375
|
||
|
|
4b4e63f622
|
||
|
|
96a2abe7e0
|
||
|
|
af48856fcd
|
||
|
|
b849e1a4b8
|
||
|
|
f58f28a2a8
|
||
|
|
8f04494ba8
|
||
|
|
8062a8ac77
|
||
|
|
4a982ae089
|
||
|
|
d9e2c4a01d
|
||
|
|
e3da95247d
|
||
|
|
ae62cbf7cd
|
||
|
|
9ca03493b2
|
||
|
|
881a222f5f
|
||
|
|
5028dd7ca1
|
||
|
|
1e2fa39193
|
||
|
|
bb91ac62b0
|
||
|
|
5eff067e75
|
No files matched your search
@@ -1,9 +1,11 @@
|
||||
# Checks on every push to main and every pull request: formatting, lints, tests, and the page's
|
||||
# JavaScript. The app is built for real only by release.yml, from a tag.
|
||||
# Checks on every push to main and dev and every pull request: formatting, lints, tests, the
|
||||
# page's scripts, the pins (tokens, CodeMirror), the public-repository hygiene -- and the app
|
||||
# started for real, on X11 and on Wayland, to prove it gets as far as its window. The packages
|
||||
# are built only by release.yml, from a tag.
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@@ -23,6 +25,8 @@ jobs:
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
cache-workspaces: src-tauri
|
||||
- name: Nothing private in a public repository
|
||||
run: scripts/hygiene.sh
|
||||
- name: The bundled LLeMbas CLI, verified
|
||||
run: scripts/fetch-cli.sh
|
||||
- name: Formatting
|
||||
@@ -34,5 +38,37 @@ jobs:
|
||||
- name: Tests
|
||||
working-directory: src-tauri
|
||||
run: cargo test --locked
|
||||
- name: The first-run page's JavaScript
|
||||
run: node --check ui/app.js
|
||||
- name: The page's scripts parse, and none sets HTML
|
||||
run: scripts/check-ui.sh
|
||||
- name: The design tokens and CodeMirror are the server's, as pinned
|
||||
run: |
|
||||
scripts/fetch-tokens.sh
|
||||
scripts/fetch-vendor.sh
|
||||
|
||||
# The app started for real: it must still be running after 25 seconds (timeout's 124), and its
|
||||
# log must not say it stopped. Once under Xvfb (X11) and once under a headless Weston (Wayland,
|
||||
# GTK's Wayland backend forced), each with a session bus (the tray and the notifications want
|
||||
# one) and an empty home, so it starts at its first-run page. Built with the custom protocol on,
|
||||
# as a release is, so the page is the bundled ui/ and not a development server.
|
||||
start:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: System libraries, and an X server and a Wayland compositor without a screen
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev \
|
||||
xvfb weston dbus libgl1-mesa-dri
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: src-tauri
|
||||
- name: The bundled LLeMbas CLI, verified
|
||||
run: scripts/fetch-cli.sh
|
||||
- name: Build (debug, with the bundled page)
|
||||
working-directory: src-tauri
|
||||
run: cargo build --locked --features tauri/custom-protocol
|
||||
- name: Starts on X11
|
||||
run: scripts/ci-start.sh x11
|
||||
- name: Starts on Wayland
|
||||
run: scripts/ci-start.sh wayland
|
||||
@@ -1,6 +1,6 @@
|
||||
# A release, from a signed tag vX.Y.Z on main: the Linux packages (.deb and AppImage) built with
|
||||
# the LLeMbas CLI inside, SHA256SUMS over them, and the GitHub Release with the tag message as its
|
||||
# notes. LLeMbas Desktop is developed on Gitea, which mirrors its commits and tags here; the builds
|
||||
# A release, from a signed tag vX.Y.Z (stable, main) or vX.Y.Z-beta.N (a beta, marked as a
|
||||
# pre-release): the Linux packages (.deb and AppImage) built with the LLeMbas CLI inside,
|
||||
# SHA256SUMS over them, and the GitHub Release with the tag message as its notes. LLeMbas Desktop is developed on Gitea, which mirrors its commits and tags here; the builds
|
||||
# happen here because they need a full Rust and WebKitGTK toolchain.
|
||||
name: release
|
||||
on:
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
tag:
|
||||
description: "The tag, e.g. v0.1.0"
|
||||
required: true
|
||||
replace:
|
||||
description: "Replace a release that already exists for this tag (people who installed it get no update)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -48,6 +52,26 @@ jobs:
|
||||
run: cargo install tauri-cli --version "^2" --locked
|
||||
- name: Build
|
||||
run: cargo tauri build --bundles deb,appimage
|
||||
# Debian sorts 1.0.0-beta.1 *above* 1.0.0 (what follows the dash is a package revision), so a
|
||||
# beta's package would make the release after it look like a downgrade, and apt would keep the
|
||||
# beta. A beta's package version says ~beta, which sorts below.
|
||||
- name: A beta's .deb sorts below its release
|
||||
run: |
|
||||
deb="$(ls src-tauri/target/release/bundle/deb/*.deb)"
|
||||
have="$(dpkg-deb -f "$deb" Version)"
|
||||
want="${VERSION/-/\~}"
|
||||
if [ "$have" != "$want" ]; then
|
||||
echo "repacking $deb: Version $have -> $want"
|
||||
work="$(mktemp -d)"
|
||||
dpkg-deb -R "$deb" "$work/pkg"
|
||||
sed -i "s/^Version: .*/Version: $want/" "$work/pkg/DEBIAN/control"
|
||||
dpkg-deb --root-owner-group -b "$work/pkg" "$deb"
|
||||
fi
|
||||
base="${VERSION%%-*}"
|
||||
if [ "$VERSION" != "$base" ]; then
|
||||
dpkg --compare-versions "$(dpkg-deb -f "$deb" Version)" lt "$base" \
|
||||
|| { echo "the beta's package does not sort below $base" >&2; exit 1; }
|
||||
fi
|
||||
- name: Collect the files and SHA256SUMS
|
||||
run: |
|
||||
mkdir dist
|
||||
@@ -62,5 +86,18 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
git tag -l --format='%(contents)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' > notes.md
|
||||
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \
|
||||
# A version is released once. Installing the same version again over a broken one is a
|
||||
# no-op for apt ("already the newest version") and the updater offers nothing, so a fix
|
||||
# is a new version -- never the old one re-released. Replacing is a deliberate act: a
|
||||
# run by hand with `replace`.
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
if [ "${{ inputs.replace || false }}" != "true" ]; then
|
||||
echo "A release for $TAG exists already. Release the fix as a new version." >&2
|
||||
exit 1
|
||||
fi
|
||||
gh release delete "$TAG" --repo "$GITHUB_REPOSITORY" --yes
|
||||
fi
|
||||
pre=()
|
||||
case "$VERSION" in *-*) pre=(--prerelease) ;; esac
|
||||
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag "${pre[@]}" \
|
||||
--title "LLeMbas Desktop $VERSION" --notes-file notes.md dist/*
|
||||
+187
@@ -6,6 +6,193 @@ All notable changes to LLeMbas Desktop. The format follows
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0-beta.1] — 2026-11-03
|
||||
|
||||
LLeMbas Desktop 1.0 becomes an app of its own: the agent on this computer by default, your
|
||||
server's chats, two explorers and a viewer -- no longer a window onto the web interface. It needs a
|
||||
LLeMbas 2.0 server.
|
||||
|
||||
|
||||
Carries LLeMbas CLI 2.0.0-beta.1. Needs a LLeMbas 2.0 server (2.0.0-beta.1 or later).
|
||||
|
||||
### Changed
|
||||
|
||||
- **The app's own interface replaces the server's web pages.** The window shows the app's page
|
||||
(`ui/`, plain HTML, CSS and JavaScript modules, no build step): the chats, the transcript with
|
||||
its steps and tool calls, the reply as it is written, the questions and approvals that wait for
|
||||
an answer, and a composer. It talks to the server's `/api/v2`.
|
||||
- **Agent chats first.** The sidebar opens on agent chats, those on this computer above those on
|
||||
your other devices; a new agent chat is started on this computer, in one of the agent's
|
||||
folders, with its mode and model. Chats on the server are the second tab.
|
||||
- **One approval signs the app in and links this computer.** The app asks the server's device
|
||||
login for its own token (your chats, as the web interface reads and writes them, and dictation)
|
||||
and, in the same approval, for the agent's; the approval opens in your browser. The agent's
|
||||
token goes straight to the built-in LLeMbas CLI on its standard input (`lembas login
|
||||
--token-stdin`), which keeps it in its own configuration as `lembas login` does; the app never
|
||||
stores it. A computer linked already keeps its link, and **Link again** under Settings is still
|
||||
there. A server that cannot give both tokens from one approval gets the old two steps.
|
||||
- The app's own token is not a device: the server lists it under *Apps*, and only the agent's
|
||||
link is among the devices an agent chat can start on.
|
||||
- The look is still the server's: its design tokens, now copied from the 2.0 server, and its
|
||||
CodeMirror build for code (`scripts/fetch-vendor.sh`, pinned in `vendor.lock.json`). The
|
||||
window takes the account's theme.
|
||||
- One window with one page: the second webview, and the Linux workaround it needed, are gone. The
|
||||
window still has the app's own title bar, and its edges resize it.
|
||||
- The page's words, old and new, are in all nine of the server's languages.
|
||||
|
||||
### Added
|
||||
|
||||
- **Two explorers, kept apart**: this computer's -- the open agent chat's folder, or one of the
|
||||
agent's folders -- read only inside the agent's folders, and the server's files, through
|
||||
`/api/v2/files` with your own access. A server file that is not text, or too large, says so in
|
||||
your language instead of failing.
|
||||
- **A viewer** beside the chat: Markdown drawn, code highlighted (read-only CodeMirror), text,
|
||||
and HTML in a sealed frame on its own address, with no access to the app, no network and no way
|
||||
to navigate. A file the agent shows (`show`) opens in it: from this computer for an agent chat
|
||||
running here, otherwise as the server keeps it (`/api/v2/chats/…/show`), checked against the
|
||||
chat's access when it is opened. A file that is gone, too large or not text says so.
|
||||
- **Native notifications** when a reply or an agent chat finishes while you are elsewhere (on by
|
||||
default; Settings, Notifications).
|
||||
- **A server older than LLeMbas 2.0 is refused** with a message saying which side to update, and so
|
||||
is a server whose app interface is newer than this app knows. What counts is the link protocol
|
||||
(3) and the app interface (`app_api` 2), not the server's version number.
|
||||
- **Settings in the app**: the server and the sign-in, the agent and its folders, notifications
|
||||
and updates. There are no admin pages.
|
||||
- When the app cannot start, it says why in `desktop.log` in its log directory, not only on a
|
||||
terminal nobody sees. A missing tray no longer stops it from starting.
|
||||
- With NVIDIA's own driver, WebKitGTK's DMA-BUF renderer is turned off before the window opens
|
||||
(unless you have set `WEBKIT_DISABLE_DMABUF_RENDERER`): that renderer leaves the window blank or
|
||||
ends the app there, on Wayland most of all.
|
||||
|
||||
### Removed
|
||||
|
||||
- The server's web interface in the window, the microphone permission it needed, and the theme
|
||||
report the window read from it. The web interface stays in the browser, and as an installable
|
||||
web app.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A release can no longer be quietly replaced under the same version: installing the same version
|
||||
again does nothing, so people with the broken copy never got the fix. The release workflow now
|
||||
refuses a tag that has a release already, unless it is run by hand to replace it.
|
||||
- A beta's `.deb` sorts below the release that follows it (`1.0.0~beta.1` before `1.0.0`), so apt
|
||||
does not keep a beta over its release.
|
||||
|
||||
## [0.7.2] — 2026-10-25
|
||||
|
||||
Linking this computer again leaves one device, and LLeMbas CLI 1.5.2 inside.
|
||||
|
||||
### Changed
|
||||
|
||||
- Carries LLeMbas CLI 1.5.2. Linking this computer again no longer leaves an offline copy of it on
|
||||
the server holding its agent chats: the old link is signed out, and a LLeMbas server 1.7.2 or
|
||||
later moves its chats to the new one. When the agent cannot connect, its log (`agent.log` in the
|
||||
app's log directory) now says why -- a certificate that is not trusted, for instance -- instead
|
||||
of only "could not reach the instance".
|
||||
|
||||
## [0.7.1] — 2026-10-24
|
||||
|
||||
The microphone works, and LLeMbas CLI 1.5.1 inside.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Dictation said the microphone was blocked, with no way to allow it.** The server's page asks
|
||||
for the microphone, and a webview grants it only if the app answers: WebKitGTK refuses whatever
|
||||
it is not answered on, and shows no prompt of its own. Desktop now allows the microphone -- the
|
||||
microphone only, and only for the server's page in the content view; a camera or a screen still
|
||||
gets the platform's refusal.
|
||||
|
||||
### Changed
|
||||
|
||||
- Carries LLeMbas CLI 1.5.1: an image counts by its size, so a screenshot no longer makes the
|
||||
agent compact.
|
||||
|
||||
## [0.7.0] — 2026-10-20
|
||||
|
||||
Carries LLeMbas CLI 1.5.0: harness spec 2.0.0 — background work as jobs the person can see,
|
||||
enter and stop, and subagents that run in the background and talk back.
|
||||
|
||||
### Changed
|
||||
|
||||
- Carries LLeMbas CLI 1.5.0. The agent on this computer runs subagents in the background, keeps
|
||||
every background command and subagent as a job of its session, and reports them to a LLeMbas
|
||||
server 1.6.0, which shows them in the chat with Stop and a message box.
|
||||
|
||||
## [0.6.0] — 2026-10-20
|
||||
|
||||
Carries LLeMbas CLI 1.4.0: harness spec 1.2.0, so the agent's memory, notes and skills are the
|
||||
same tools as the server's.
|
||||
|
||||
### Changed
|
||||
|
||||
- Carries LLeMbas CLI 1.4.0. The agent on this computer and a LLeMbas server 1.5.0 now send a
|
||||
model the same library tools — `memory`, `note_view`, `note_manage`, `skills_list`,
|
||||
`skill_view`, `skill_manage` — whether the agent uses its own library or the server's.
|
||||
|
||||
## [0.5.0] — 2026-10-18
|
||||
|
||||
Carries LLeMbas CLI 1.3.0: a plan in every agent chat, the instance's instructions and
|
||||
knowledge scope on the device, Edit and Regenerate on agent chats. The app's look comes from
|
||||
the server's tokens and follows the page's theme; its own pages speak nine languages.
|
||||
|
||||
### Changed
|
||||
|
||||
- The app's own pages take their look from the server's `tokens.css`, copied by
|
||||
`scripts/fetch-tokens.sh` from a server tag and pinned in `tokens.lock.json` (the script with
|
||||
no argument verifies the copy). The hand copy had drifted: `--danger`, `--control-h` and
|
||||
`--text-xl` were not the server's. The pages name no colour, size or space of their own, and
|
||||
no font is bundled: the font rules are left out of the copy, so the title bar and the
|
||||
first-run page are set in the system's monospace face.
|
||||
- The window's frame is painted in the theme's border colour read out of `tokens.css` at build
|
||||
time (`build.rs`), not a value written in Rust, and it changes with the theme.
|
||||
- The first-run page and the title bar speak the system's language when it is one of the
|
||||
server's nine (Czech, German, Spanish, French, Hungarian, Italian, Polish, Slovak; English
|
||||
otherwise). `ui/i18n.js` is generated by `scripts/gen-i18n.py`: the sentences the server also
|
||||
has are translated as the server translates them, the app's own are in the script.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The title bar followed the desktop's light or dark setting, not the account's theme, so it
|
||||
could be dark over the parchment (Shire) page. It now follows the server page: a script in the
|
||||
content view reports the page's theme to the app, which paints the frame, the title bar and
|
||||
the first-run page in it and remembers it for the next start. The server's pages still get no
|
||||
IPC; the report is a navigation attempt the app reads and refuses.
|
||||
- The close button's hover colour was a literal white; it is the theme's inverse ink.
|
||||
|
||||
## [0.4.0] — 2026-10-14
|
||||
|
||||
Carries LLeMbas CLI 1.2.0, whose agent stays linked before any folder is allowed and follows
|
||||
changes to its folders without a restart.
|
||||
|
||||
### Added
|
||||
|
||||
- **Project folders**: Settings… (in the title bar's and the tray's menu) lists the folders the
|
||||
agent may work in, with *Add folder…* (the system's folder dialog), *Remove*, the most an agent
|
||||
chat may do there, and whether the server may open a terminal. Turning the agent on with no
|
||||
folder chosen opens this page. Changes go through the bundled CLI (`lembas remote`).
|
||||
- The server is told it is shown in LLeMbas Desktop and on which computer, so it lists this
|
||||
computer's device as **Local**, first.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The title bar's menu opens under the button, not in the middle of the window.
|
||||
- The agent no longer shows a green light while it may open no folder: the title bar says
|
||||
*agent: no folders*.
|
||||
|
||||
## [0.3.0] — 2026-10-14
|
||||
|
||||
### Added
|
||||
|
||||
- Updates: the app looks for a new release at start and every six hours, shows an **update**
|
||||
button in the title bar when there is one, and installs it (the .deb with the system's password
|
||||
prompt, an AppImage in place) after checking its signature. The menu can also check by hand.
|
||||
|
||||
### Fixed
|
||||
|
||||
- On Linux the server's page took only the lower half of the window, under an empty title bar
|
||||
area. The title bar is now its fixed height and the page takes the rest; the frame you resize
|
||||
the window by is drawn around both.
|
||||
|
||||
## [0.2.0] — 2026-10-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
# LLeMbas Desktop
|
||||
|
||||
Your [LLeMbas](https://llembas.eu) server in a window of its own, with the agent on this computer.
|
||||
The [LLeMbas](https://llembas.eu) agent on this computer, and the chats on your LLeMbas server, in
|
||||
an app of its own.
|
||||
|
||||
- **The whole web UI**: chats, agent chats, the library, reports, settings. Same account, models,
|
||||
permissions and credits as in the browser; the app is a front end to the server you already run.
|
||||
- **The agent, built in.** LLeMbas Desktop carries the [LLeMbas CLI](https://github.com/LLeMbas/LLeMbas-CLI)
|
||||
and runs it in the background as this computer's device, so agent chats started on the server
|
||||
can work in your projects here, without a terminal.
|
||||
- **Its own window**: a LLeMbas title bar with the agent's state and a menu (start and stop the
|
||||
agent, link the computer, reload, change server, quit). Close the window and the agent keeps
|
||||
running in the tray, which has the same menu.
|
||||
- **The agent, built in, by default.** LLeMbas Desktop carries the
|
||||
[LLeMbas CLI](https://github.com/LLeMbas/LLeMbas-CLI) and runs it in the background as this
|
||||
computer's device. Agent chats work in your project folders here, without a terminal -- start
|
||||
them, follow them and answer them in the app.
|
||||
- **Your server's chats** beside them: the same account, models and permissions as in the browser.
|
||||
- **Two explorers, kept apart**: the agent's working directory on this computer, and your files
|
||||
on the server.
|
||||
- **A viewer** for what you open and for what the agent shows you: Markdown drawn, code
|
||||
highlighted, text, and HTML in a sealed frame that can reach neither the app nor the network.
|
||||
- **Notifications** when a reply or an agent's work finishes while you are elsewhere.
|
||||
- **Its own window and tray**: close the window and the agent keeps running in the tray.
|
||||
|
||||
> **Alpha.** Linux first (`.deb` and AppImage, x86-64). Windows and macOS later.
|
||||
There are no admin pages: an administrator uses the server's web interface for that.
|
||||
|
||||
> **Beta (1.0).** Needs a **LLeMbas 2.0 server** or later; an older one is refused with a message
|
||||
> saying so. Linux first (`.deb` and AppImage, x86-64). Windows and macOS later.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -20,23 +27,55 @@ Download the `.deb` or the AppImage from the
|
||||
`SHA256SUMS` in the same release.
|
||||
|
||||
```sh
|
||||
sudo apt install ./llembas-desktop_*_amd64.deb # Debian, Ubuntu (replaces 0.1.0's l-le-mbas)
|
||||
sudo apt install ./llembas-desktop_*_amd64.deb # Debian, Ubuntu
|
||||
chmod +x llembas-desktop_*.AppImage && ./llembas-desktop_*.AppImage
|
||||
```
|
||||
|
||||
Installing a `.deb` of the version you already have changes nothing (apt calls it "already the
|
||||
newest version"); `sudo apt reinstall ./llembas-desktop_*_amd64.deb` installs it again. If the app
|
||||
does not start, the reason is in `~/.local/share/eu.llembas.desktop/logs/desktop.log`.
|
||||
|
||||
On GNOME the tray needs the *AppIndicator* extension; without it, the title bar's menu has
|
||||
everything, and starting the app again brings a hidden window back.
|
||||
|
||||
## Updates
|
||||
|
||||
LLeMbas Desktop looks for a new release when it starts and every six hours. When there is one, an
|
||||
**update** button appears in the title bar (and *Install LLeMbas x.y.z…* in its menu; *Check for
|
||||
updates…* otherwise). Installing asks for your password to update the `.deb` (an AppImage updates
|
||||
itself), then the app starts again. Every package is checked against the release signing key
|
||||
built into the app before anything is installed. Betas are not offered as updates.
|
||||
|
||||
## Use
|
||||
|
||||
1. Start LLeMbas and enter your server's address. Sign in there as you would in a browser.
|
||||
2. From the tray, **Link this computer**. The approval page opens in the window; approve it.
|
||||
3. The agent starts. In the server's new-chat screen, choose this computer for an agent chat.
|
||||
1. Start LLeMbas and enter your server's address. **Sign in**: the approval page opens in your
|
||||
browser, where you are signed in already; approve the code shown in the app. This one approval
|
||||
lets the app read and write your chats, as the web interface does, and links this computer as
|
||||
the device the agent runs on: the app hands the agent's token to its LLeMbas CLI, which keeps it
|
||||
in its own configuration. The agent starts. (A computer linked already keeps its link; on a
|
||||
server that cannot give both from one approval, **Link this computer** under **Settings** is a
|
||||
second one.)
|
||||
2. Choose the folders the agent may work in (Settings, *Add folder…*); a new computer allows none
|
||||
until you do.
|
||||
3. **New agent chat**: pick one of those folders, a mode and a model, and write.
|
||||
|
||||
The agent is the LLeMbas CLI's `lembas service`, using the CLI's own configuration
|
||||
(`~/.config/lembas`). The app's copy of the CLI lives in its own directory
|
||||
(`/usr/lib/llembas-desktop/`), not on your `PATH`. If you also installed the CLI and run `lembas service` yourself, the app sees
|
||||
it and does not start a second one: they are the same device.
|
||||
(`/usr/lib/llembas-desktop/`), not on your `PATH`. If you also installed the CLI and run
|
||||
`lembas service` yourself, the app sees it and does not start a second one: they are the same
|
||||
device.
|
||||
|
||||
### What can reach what
|
||||
|
||||
- The app's page is its own (`ui/`), and it is the only content with access to the app. No page
|
||||
of the server is ever loaded into the window, and the window goes nowhere else.
|
||||
- The app's sign-in token stays in the app's Rust half (`account.json` in the app's config
|
||||
directory, readable by you only); the page asks for the server's `/api/v2` through it and never
|
||||
holds the token.
|
||||
- The explorer reads only inside the agent's folders.
|
||||
- HTML is shown in a sealed frame on an address of its own: scripts in it run, but it has no
|
||||
access to the app, no network and no way to navigate anywhere.
|
||||
- Links in a reply open in your browser, and only web and mail links.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -45,14 +84,32 @@ Needs Rust (stable) and, on Linux, WebKitGTK 4.1 and an AppIndicator library:
|
||||
```sh
|
||||
sudo apt install libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev
|
||||
scripts/fetch-cli.sh # the bundled CLI, checked against its release signature
|
||||
scripts/fetch-tokens.sh # verifies ui/tokens.css against tokens.lock.json
|
||||
scripts/fetch-vendor.sh # verifies ui/vendor/ against vendor.lock.json
|
||||
cargo install tauri-cli --version "^2" --locked
|
||||
cargo tauri dev # or: cargo tauri build
|
||||
```
|
||||
|
||||
There is no Node and no bundler: the app's one local page is plain HTML, CSS and JavaScript in
|
||||
`ui/`. Everything else on screen is the server's own web UI. The version of the CLI it carries is
|
||||
in `lembas-cli.version`.
|
||||
There is no Node and no bundler: the app's page is plain HTML, CSS and JavaScript modules in
|
||||
`ui/`. The version of the CLI it carries is in `lembas-cli.version`.
|
||||
|
||||
Three things in `ui/` are copied from the LLeMbas server and committed, each pinned:
|
||||
|
||||
- `ui/tokens.css`, the server's design tokens: `scripts/fetch-tokens.sh <server ref>` from a
|
||||
checkout of the server beside this repository (`../LLeMbas`, or `LEMBAS_SERVER`), pinned in
|
||||
`tokens.lock.json`.
|
||||
- `ui/vendor/`, the server's vendored CodeMirror build: `scripts/fetch-vendor.sh <server ref>`
|
||||
checks every module against the server's own pin before copying it, and records both hashes in
|
||||
`vendor.lock.json`. The one change: the modules import each other by file name, since the page
|
||||
has no import map.
|
||||
- `ui/i18n.js`, the page's words in the server's languages: `scripts/gen-i18n.py`, from the
|
||||
server's catalogues and the script's own table. Run it when a sentence on the page changes.
|
||||
|
||||
`scripts/check-ui.sh` parses the page's scripts and refuses any that sets HTML from a string;
|
||||
`scripts/hygiene.sh` keeps private names out of this public repository. CI runs both, and starts
|
||||
the built app under X11 and under Wayland (`scripts/ci-start.sh`).
|
||||
|
||||
## Licence
|
||||
|
||||
MIT; see [LICENSE](LICENSE). The bundled LLeMbas CLI is MIT too, with its own third-party notices.
|
||||
CodeMirror and Lezer (in `ui/vendor/`) are MIT.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.1.0
|
||||
2.0.0-beta.1
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# The page's JavaScript parses: the app's modules and the vendored ones as ES modules (a plain
|
||||
# `node --check file.js` treats a .js file as CommonJS and passes a module with a syntax error in
|
||||
# it), the generated i18n.js as the classic script it is.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
bad=0
|
||||
for f in ui/js/*.js ui/vendor/*.mjs; do
|
||||
node --input-type=module --check < "$f" || { echo "check-ui: $f does not parse" >&2; bad=1; }
|
||||
done
|
||||
node --check ui/i18n.js || { echo "check-ui: ui/i18n.js does not parse" >&2; bad=1; }
|
||||
# Nothing the page draws is set as HTML: text goes in as text (ui/js/dom.js).
|
||||
if grep -nE '\.(innerHTML|outerHTML)\s*=|insertAdjacentHTML|document\.write' ui/js/*.js; then
|
||||
echo "check-ui: the lines above set HTML from a string" >&2
|
||||
bad=1
|
||||
fi
|
||||
[ "$bad" -eq 0 ] && echo "check-ui: the page's scripts parse, and none sets HTML"
|
||||
exit "$bad"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the built app on a display without a screen and check it is still running 25 seconds
|
||||
# later, with nothing in its log saying it stopped. For CI (ci.yml, job `start`):
|
||||
#
|
||||
# scripts/ci-start.sh x11 under Xvfb
|
||||
# scripts/ci-start.sh wayland under a headless Weston, GTK's Wayland backend forced
|
||||
#
|
||||
# The app starts with an empty home, so it shows its first-run page and talks to no server.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
APP="${LLEMBAS_DESKTOP_BIN:-src-tauri/target/debug/llembas-desktop}"
|
||||
[ -x "$APP" ] || { echo "ci-start: no app at $APP; build it first" >&2; exit 1; }
|
||||
how="${1:?usage: ci-start.sh x11|wayland}"
|
||||
|
||||
home="$(mktemp -d)"
|
||||
runtime="$(mktemp -d)"
|
||||
chmod 700 "$runtime"
|
||||
pids=()
|
||||
cleanup() {
|
||||
for p in "${pids[@]}"; do kill "$p" 2>/dev/null || true; done
|
||||
rm -rf "$home" "$runtime"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
export HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_DATA_HOME="$home/.local/share" \
|
||||
XDG_STATE_HOME="$home/.local/state" XDG_CACHE_HOME="$home/.cache" XDG_RUNTIME_DIR="$runtime"
|
||||
# No GPU on a runner: Mesa's software renderer.
|
||||
export LIBGL_ALWAYS_SOFTWARE=1
|
||||
log="$XDG_DATA_HOME/eu.llembas.desktop/logs/desktop.log"
|
||||
|
||||
case "$how" in
|
||||
x11)
|
||||
run=(xvfb-run -a -s "-screen 0 1280x840x24")
|
||||
;;
|
||||
wayland)
|
||||
weston --backend=headless-backend.so --socket=wayland-ci --idle-time=0 --use-pixman \
|
||||
> "$runtime/weston.log" 2>&1 &
|
||||
pids+=("$!")
|
||||
for _ in $(seq 1 50); do
|
||||
[ -S "$runtime/wayland-ci" ] && break
|
||||
sleep 0.2
|
||||
done
|
||||
[ -S "$runtime/wayland-ci" ] || { echo "ci-start: weston did not start" >&2; cat "$runtime/weston.log" >&2; exit 1; }
|
||||
export WAYLAND_DISPLAY=wayland-ci GDK_BACKEND=wayland
|
||||
unset DISPLAY
|
||||
run=()
|
||||
;;
|
||||
*)
|
||||
echo "ci-start: x11 or wayland, not $how" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
code=0
|
||||
dbus-run-session -- "${run[@]}" timeout 25 "$APP" > "$runtime/app.log" 2>&1 || code=$?
|
||||
echo "--- the app's output ($how)"
|
||||
cat "$runtime/app.log"
|
||||
if [ -f "$log" ]; then
|
||||
echo "--- desktop.log"
|
||||
cat "$log"
|
||||
fi
|
||||
if [ "$code" -ne 124 ]; then
|
||||
echo "ci-start: on $how the app ended by itself within 25 seconds (exit $code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$log" ] && grep -q 'stopped:\|could not start\|could not open the window' "$log"; then
|
||||
echo "ci-start: on $how the app logged a failure" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ci-start: on $how the app was still running after 25 seconds"
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Put the LLeMbas server's design tokens where the app's own pages read them from:
|
||||
# ui/tokens.css (a copy of the server's static/css/tokens.css, never edited by hand)
|
||||
#
|
||||
# scripts/fetch-tokens.sh <server-ref> copy the tokens at that ref (a tag such as v1.3.0,
|
||||
# or HEAD) and write tokens.lock.json
|
||||
# scripts/fetch-tokens.sh verify: ui/tokens.css matches tokens.lock.json
|
||||
#
|
||||
# The server is a git checkout, LLeMbas beside this repository by default (LLEMBAS_SERVER names
|
||||
# another). The one change made to the copy: the @font-face rules are left out, with the comment
|
||||
# above them. They name woff2 files the server vendors and the app does not carry; the app's pages
|
||||
# fall through the same `--font-ui` stack to the system's monospace face, and without the rules
|
||||
# the webview does not ask for two files that are not there.
|
||||
#
|
||||
# tokens.lock.json records the ref, the commit it resolved to, and the SHA-256 of ui/tokens.css
|
||||
# as written, which is what verify checks. Like fetch-cli.sh and lembas-cli.version: the file in
|
||||
# the tree says which server version the look comes from, and CI refuses a copy that drifted.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SERVER="${LEMBAS_SERVER:-../LLeMbas}"
|
||||
SOURCE="src/lembas/web/static/css/tokens.css"
|
||||
OUT="ui/tokens.css"
|
||||
LOCK="tokens.lock.json"
|
||||
|
||||
sha() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
[ -f "$LOCK" ] || { echo "fetch-tokens: no $LOCK; run with a server ref first" >&2; exit 1; }
|
||||
[ -f "$OUT" ] || { echo "fetch-tokens: no $OUT; run with a server ref first" >&2; exit 1; }
|
||||
want="$(sed -n -E 's/^ *"sha256": *"([0-9a-f]{64})".*/\1/p' "$LOCK" | head -n1)"
|
||||
ref="$(sed -n -E 's/^ *"tag": *"([^"]+)".*/\1/p' "$LOCK" | head -n1)"
|
||||
have="$(sha "$OUT")"
|
||||
if [ "$want" != "$have" ]; then
|
||||
echo "fetch-tokens: $OUT does not match $LOCK (server $ref): $have, expected $want" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "fetch-tokens: $OUT matches $LOCK (server $ref)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REF="$1"
|
||||
[ -d "$SERVER/.git" ] || [ -f "$SERVER/.git" ] \
|
||||
|| { echo "fetch-tokens: $SERVER is not a git checkout of the LLeMbas server (set LEMBAS_SERVER)" >&2; exit 1; }
|
||||
commit="$(git -C "$SERVER" rev-parse --verify --quiet "$REF^{commit}")" \
|
||||
|| { echo "fetch-tokens: $REF is not a commit in $SERVER" >&2; exit 1; }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
git -C "$SERVER" show "$commit:$SOURCE" > "$work/tokens.css" \
|
||||
|| { echo "fetch-tokens: $SOURCE is not in $SERVER at $REF" >&2; exit 1; }
|
||||
|
||||
# A comment immediately before an @font-face goes with it; then every @font-face block.
|
||||
perl -0pe 's#/\*(?:(?!\*/).)*?\*/\s*(?=\@font-face)##gs; s#\@font-face\s*\{[^}]*\}\s*##g' \
|
||||
"$work/tokens.css" > "$work/body.css"
|
||||
grep -q -- '--border:' "$work/body.css" && grep -q -- '--bg:' "$work/body.css" \
|
||||
|| { echo "fetch-tokens: $SOURCE at $REF has no --border or --bg: not the tokens file" >&2; exit 1; }
|
||||
! grep -q '@font-face' "$work/body.css" \
|
||||
|| { echo "fetch-tokens: an @font-face survived the strip; fix the script" >&2; exit 1; }
|
||||
|
||||
{
|
||||
printf '/* LLeMbas %s (%s) static/css/tokens.css, copied by scripts/fetch-tokens.sh: do not edit.\n' "$REF" "${commit:0:12}"
|
||||
printf ' The font-face rules are left out; the app bundles no font. */\n'
|
||||
cat "$work/body.css"
|
||||
} > "$work/out.css"
|
||||
|
||||
cp "$work/out.css" "$OUT"
|
||||
printf '{\n "tag": "%s",\n "commit": "%s",\n "sha256": "%s"\n}\n' "$REF" "$commit" "$(sha "$OUT")" > "$LOCK"
|
||||
echo "fetch-tokens: LLeMbas $REF (${commit:0:12}) $SOURCE -> $OUT; $LOCK written"
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# Put the server's vendored CodeMirror build where the app's own pages read it from:
|
||||
# ui/vendor/*.mjs (copies of the server's static/vendor/cm-*.mjs and lezer-*.mjs)
|
||||
#
|
||||
# scripts/fetch-vendor.sh <server-ref> copy the modules at that ref (a tag such as
|
||||
# v2.0.0-beta.1, or a branch) and write vendor.lock.json
|
||||
# scripts/fetch-vendor.sh verify: ui/vendor matches vendor.lock.json
|
||||
#
|
||||
# The server is a git checkout, LLeMbas beside this repository by default (LLEMBAS_SERVER names
|
||||
# another). Each module is checked against the server's own pin (its scripts/vendor.lock.json, at
|
||||
# the same ref) before it is copied, so what the app carries is exactly what the server serves.
|
||||
#
|
||||
# The one change made to a copy: the server loads these modules through an import map
|
||||
# ("@codemirror/view" -> its file), which the app's pages do not have -- an inline import map is a
|
||||
# script the content security policy would have to allow. So the bare names in `import`/`from`
|
||||
# are rewritten to the sibling files ("./cm-view.mjs"), and nothing else is touched.
|
||||
#
|
||||
# vendor.lock.json records the ref, the commit it resolved to, and for every file the server's
|
||||
# SHA-256 and the copy's, which is what verify checks. Like fetch-tokens.sh: the file in the tree
|
||||
# says which server version the editor comes from, and CI refuses a copy that drifted.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SERVER="${LEMBAS_SERVER:-../LLeMbas}"
|
||||
SOURCE="src/lembas/web/static/vendor"
|
||||
SERVER_LOCK="scripts/vendor.lock.json"
|
||||
OUT="ui/vendor"
|
||||
LOCK="vendor.lock.json"
|
||||
|
||||
sha() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
[ -f "$LOCK" ] || { echo "fetch-vendor: no $LOCK; run with a server ref first" >&2; exit 1; }
|
||||
ref="$(sed -n -E 's/^ *"tag": *"([^"]+)".*/\1/p' "$LOCK" | head -n1)"
|
||||
bad=0
|
||||
count=0
|
||||
# One file a line: "name": {"server": "…", "copy": "…"}
|
||||
while IFS=$'\t' read -r name want; do
|
||||
count=$((count + 1))
|
||||
if [ ! -f "$OUT/$name" ]; then
|
||||
echo "fetch-vendor: $OUT/$name is missing" >&2
|
||||
bad=1
|
||||
elif [ "$(sha "$OUT/$name")" != "$want" ]; then
|
||||
echo "fetch-vendor: $OUT/$name does not match $LOCK (server $ref)" >&2
|
||||
bad=1
|
||||
fi
|
||||
done < <(sed -n -E 's/^ *"([a-z0-9-]+\.mjs)": *\{"server": *"[0-9a-f]{64}", *"copy": *"([0-9a-f]{64})"\}.*/\1\t\2/p' "$LOCK")
|
||||
for f in "$OUT"/*.mjs; do
|
||||
grep -q "\"$(basename "$f")\"" "$LOCK" || { echo "fetch-vendor: $f is not in $LOCK" >&2; bad=1; }
|
||||
done
|
||||
[ "$count" -gt 0 ] || { echo "fetch-vendor: $LOCK lists no file" >&2; exit 1; }
|
||||
[ "$bad" -eq 0 ] || exit 1
|
||||
echo "fetch-vendor: $count files in $OUT match $LOCK (server $ref)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REF="$1"
|
||||
[ -d "$SERVER/.git" ] || [ -f "$SERVER/.git" ] \
|
||||
|| { echo "fetch-vendor: $SERVER is not a git checkout of the LLeMbas server (set LEMBAS_SERVER)" >&2; exit 1; }
|
||||
commit="$(git -C "$SERVER" rev-parse --verify --quiet "$REF^{commit}")" \
|
||||
|| { echo "fetch-vendor: $REF is not a commit in $SERVER" >&2; exit 1; }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
git -C "$SERVER" show "$commit:$SERVER_LOCK" > "$work/server-lock.json" \
|
||||
|| { echo "fetch-vendor: $SERVER_LOCK is not in $SERVER at $REF" >&2; exit 1; }
|
||||
names="$(git -C "$SERVER" ls-tree --name-only "$commit" "$SOURCE/" | sed -n -E 's#^.*/((cm|lezer)-[a-z0-9-]+\.mjs)$#\1#p' | sort)"
|
||||
[ -n "$names" ] || { echo "fetch-vendor: no CodeMirror modules in $SOURCE at $REF" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$work/out"
|
||||
entries=()
|
||||
for name in $names; do
|
||||
git -C "$SERVER" show "$commit:$SOURCE/$name" > "$work/$name"
|
||||
pinned="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], {}).get("sha256", ""))' "$work/server-lock.json" "$name")"
|
||||
have="$(sha "$work/$name")"
|
||||
[ -n "$pinned" ] || { echo "fetch-vendor: $name is not pinned in the server's $SERVER_LOCK" >&2; exit 1; }
|
||||
[ "$pinned" = "$have" ] || { echo "fetch-vendor: $name does not match the server's pin ($have, pinned $pinned)" >&2; exit 1; }
|
||||
# "@codemirror/view" -> "./cm-view.mjs", "@lezer/lr" -> "./lezer-lr.mjs", "codemirror" -> "./cm-codemirror.mjs".
|
||||
perl -pe 's#\b(from|import)(\s*)"\@codemirror/([a-z-]+)"#$1$2"./cm-$3.mjs"#g; s#\b(from|import)(\s*)"\@lezer/([a-z-]+)"#$1$2"./lezer-$3.mjs"#g; s#\b(from|import)(\s*)"codemirror"#$1$2"./cm-codemirror.mjs"#g' \
|
||||
"$work/$name" > "$work/out/$name"
|
||||
if grep -E -q '(from|import)\s*"(@codemirror|@lezer)/' "$work/out/$name"; then
|
||||
echo "fetch-vendor: a bare module name survived in $name; fix the script" >&2
|
||||
exit 1
|
||||
fi
|
||||
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$(sha "$work/out/$name")")")
|
||||
done
|
||||
# Every module a copy imports must be one of the copies.
|
||||
for f in "$work"/out/*.mjs; do
|
||||
while read -r dep; do
|
||||
[ -f "$work/out/$dep" ] || { echo "fetch-vendor: $(basename "$f") imports $dep, which is not copied" >&2; exit 1; }
|
||||
done < <(grep -o -E '(from|import)\s*"\./[a-z0-9-]+\.mjs"' "$f" | sed -E 's/.*"\.\/([^"]+)"/\1/' | sort -u)
|
||||
done
|
||||
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT"
|
||||
cp "$work"/out/*.mjs "$OUT/"
|
||||
{
|
||||
printf '{\n "tag": "%s",\n "commit": "%s",\n "files": {\n' "$REF" "$commit"
|
||||
last=$((${#entries[@]} - 1))
|
||||
for i in "${!entries[@]}"; do
|
||||
if [ "$i" -lt "$last" ]; then printf '%s,\n' "${entries[$i]}"; else printf '%s\n' "${entries[$i]}"; fi
|
||||
done
|
||||
printf ' }\n}\n'
|
||||
} > "$LOCK"
|
||||
echo "fetch-vendor: LLeMbas $REF (${commit:0:12}) $SOURCE -> $OUT (${#entries[@]} files); $LOCK written"
|
||||
Executable
+1083
File diff suppressed because it is too large.
Load diff
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# The repository is public: nothing in it may name the private estate it was written on.
|
||||
# The LLeMbas section rule ("everything public is current-state and estate-free"), as a check
|
||||
# that runs in CI on every push rather than a sentence somebody has to remember.
|
||||
#
|
||||
# Patterns are the kinds of thing that leak: a private domain, an internal `.lan` name, a
|
||||
# container number, a private address range, a machine's name. This file is the one place that
|
||||
# may spell them, and is skipped.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
patterns=(
|
||||
'houmeres'
|
||||
'\b[a-z0-9-]+\.lan\b'
|
||||
'\bCT ?2[0-9]{2}\b'
|
||||
'\b10\.0\.0\.[0-9]+'
|
||||
'\b192\.168\.[0-9]+\.[0-9]+'
|
||||
'\bgamebox\b'
|
||||
'\bthor\b'
|
||||
)
|
||||
found=0
|
||||
for pattern in "${patterns[@]}"; do
|
||||
if git ls-files -z | grep -zv '^scripts/hygiene.sh$' | xargs -0 grep -nIEi -- "$pattern" ; then
|
||||
echo "hygiene: the lines above match /$pattern/" >&2
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
[ "$found" -eq 0 ] && echo "hygiene: nothing private in the tracked files"
|
||||
exit "$found"
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Offer a release to installed copies of LLeMbas Desktop: sign its packages with the updater key
|
||||
# and write the update manifest the app reads (https://llembas.eu/desktop/latest.json).
|
||||
#
|
||||
# MINISIGN_KEY=path/to/updater.key scripts/publish-update.sh v0.3.0 path/to/LLeMbas-Website
|
||||
#
|
||||
# The packages are the ones release.yml published on GitHub for the tag, checked against that
|
||||
# release's SHA256SUMS first. The app verifies each package's signature with the public key in
|
||||
# src-tauri/tauri.conf.json before installing it, so the manifest's host need not be trusted.
|
||||
# This writes site/desktop/latest.json in the website's checkout; committing and pushing it is
|
||||
# what publishes the update.
|
||||
set -euo pipefail
|
||||
TAG="${1:?usage: publish-update.sh vX.Y.Z path/to/LLeMbas-Website}"
|
||||
SITE="${2:?usage: publish-update.sh vX.Y.Z path/to/LLeMbas-Website}"
|
||||
KEY="${MINISIGN_KEY:?set MINISIGN_KEY to the minisign secret key of the updater}"
|
||||
VERSION="${TAG#v}"
|
||||
BASE="https://github.com/LLeMbas/LLeMbas-Desktop/releases/download/$TAG"
|
||||
DEB="llembas-desktop_${VERSION}_amd64.deb"
|
||||
APPIMAGE="llembas-desktop_${VERSION}_amd64.AppImage"
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
for f in SHA256SUMS "$DEB" "$APPIMAGE"; do
|
||||
curl -fsSL --retry 3 -o "$work/$f" "$BASE/$f"
|
||||
done
|
||||
[ "$(sed -n -E 's/^# llembas-desktop //p' "$work/SHA256SUMS" | head -n1)" = "$VERSION" ] \
|
||||
|| { echo "publish-update: SHA256SUMS is not for $VERSION" >&2; exit 1; }
|
||||
(cd "$work" && grep -E " ($DEB|$APPIMAGE)\$" SHA256SUMS | sha256sum -c --strict --quiet -) \
|
||||
|| { echo "publish-update: the packages do not match SHA256SUMS" >&2; exit 1; }
|
||||
|
||||
sig() {
|
||||
# Tab-separated, with the version: the app refuses a package whose signature names another one.
|
||||
minisign -S -s "$KEY" -m "$work/$1" -x "$work/$1.minisig" \
|
||||
-t "$(printf 'timestamp:%s\tfile:%s\tversion:%s' "$(date +%s)" "$1" "$VERSION")" >/dev/null
|
||||
base64 -w0 "$work/$1.minisig"
|
||||
}
|
||||
notes="$(git tag -l --format='%(contents)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' | tail -n +3)"
|
||||
|
||||
mkdir -p "$SITE/site/desktop"
|
||||
jq -n --arg v "$VERSION" --arg notes "$notes" --arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg deb_url "$BASE/$DEB" --arg deb_sig "$(sig "$DEB")" \
|
||||
--arg app_url "$BASE/$APPIMAGE" --arg app_sig "$(sig "$APPIMAGE")" \
|
||||
'{version: $v, notes: $notes, pub_date: $date, platforms: {
|
||||
"linux-x86_64-deb": {url: $deb_url, signature: $deb_sig},
|
||||
"linux-x86_64-appimage": {url: $app_url, signature: $app_sig}}}' \
|
||||
> "$SITE/site/desktop/latest.json"
|
||||
echo "publish-update: wrote $SITE/site/desktop/latest.json for $VERSION — commit and push it to publish"
|
||||
Generated
+415
-10
@@ -47,6 +47,15 @@ version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -460,6 +469,17 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -560,6 +580,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
@@ -708,6 +737,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -976,6 +1016,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
@@ -1266,6 +1316,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1520,6 +1571,21 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.21"
|
||||
@@ -1539,9 +1605,11 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1844,6 +1912,36 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.21",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -1987,7 +2085,7 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
|
||||
[[package]]
|
||||
name = "llembas-desktop"
|
||||
version = "0.2.0"
|
||||
version = "1.0.0-beta.1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"serde",
|
||||
@@ -1995,8 +2093,10 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
@@ -2015,6 +2115,20 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.39.0"
|
||||
@@ -2047,6 +2161,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2135,6 +2255,20 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4587364a9a0074333429b3df75a30a205340c56a536ca3eb6ca0e59b87bbf8af"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -2312,6 +2446,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2392,6 +2538,12 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2408,6 +2560,20 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -2725,6 +2891,23 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -2813,15 +2996,20 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -2920,11 +3108,23 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"openssl-probe 0.1.6",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
|
||||
dependencies = [
|
||||
"openssl-probe 0.2.1",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework 3.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2945,6 +3145,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs 0.8.5",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.15"
|
||||
@@ -3050,6 +3277,19 @@ dependencies = [
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.17.0"
|
||||
@@ -3255,7 +3495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -3281,6 +3521,22 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.4"
|
||||
@@ -3462,6 +3718,27 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -3493,7 +3770,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -3526,6 +3803,17 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -3549,7 +3837,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -3703,6 +3991,26 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.7.1"
|
||||
@@ -3741,6 +4049,40 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "855a336eb389d2c1502244fd48fa2d8cb487a430f691bbaed025195f99a04489"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"libc",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.61.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.12.1"
|
||||
@@ -3751,7 +4093,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -3774,7 +4116,7 @@ checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -3841,6 +4183,17 @@ dependencies = [
|
||||
"toml 1.1.8+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -3963,6 +4316,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.20"
|
||||
@@ -4227,7 +4590,7 @@ dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4489,6 +4852,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -4671,6 +5043,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -4987,7 +5370,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
@@ -5034,6 +5417,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
@@ -5187,6 +5580,18 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.14.2",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "llembas-desktop"
|
||||
version = "0.2.0"
|
||||
description = "LLeMbas Desktop: your LLeMbas server in a window, with the agent on this computer"
|
||||
version = "1.0.0-beta.1"
|
||||
description = "LLeMbas Desktop: the agent on this computer and your LLeMbas server's chats, in an app of its own"
|
||||
authors = ["Jaroslav Beneš"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/LLeMbas/LLeMbas-Desktop"
|
||||
@@ -17,10 +17,12 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon", "unstable"] }
|
||||
tauri = { version = "2.12", features = ["tray-icon"] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ureq = { version = "2", default-features = false, features = ["tls", "native-certs", "json"] }
|
||||
|
||||
@@ -1,3 +1,72 @@
|
||||
//! Besides Tauri's own build step: the two themes' page colour, which the window itself paints
|
||||
//! before the page has (so a dark theme does not start white), read out of `ui/tokens.css` (the server's tokens, fetched by `scripts/fetch-tokens.sh`) into
|
||||
//! `$OUT_DIR/tokens.rs`, so no colour is written in Rust. `lib.rs` includes that file.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// The themes the app knows, by the name `tokens.css` selects them with (`data-theme`), and the
|
||||
/// Rust names the constants get.
|
||||
const THEMES: [(&str, &str); 2] = [("moria", "MORIA"), ("shire", "SHIRE")];
|
||||
/// The tokens taken from each theme, by the custom property and the constant's suffix.
|
||||
const TOKENS: [(&str, &str); 1] = [("--bg", "BG")];
|
||||
|
||||
fn main() {
|
||||
let tokens = Path::new("../ui/tokens.css");
|
||||
println!("cargo:rerun-if-changed={}", tokens.display());
|
||||
let css = fs::read_to_string(tokens)
|
||||
.unwrap_or_else(|e| panic!("{}: {e}; run scripts/fetch-tokens.sh", tokens.display()));
|
||||
let mut out = String::from(
|
||||
"// Generated by build.rs from ui/tokens.css: the colours the window paints itself.\n",
|
||||
);
|
||||
for (theme, name) in THEMES {
|
||||
let block = theme_block(&css, theme).unwrap_or_else(|| {
|
||||
panic!("ui/tokens.css has no `:root[data-theme=\"{theme}\"]` block")
|
||||
});
|
||||
for (token, suffix) in TOKENS {
|
||||
let (r, g, b) = hex_token(block, token).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"ui/tokens.css: `{token}` of the {theme} theme is not a six-digit hex colour"
|
||||
)
|
||||
});
|
||||
out.push_str(&format!(
|
||||
"pub const {name}_{suffix}: (u8, u8, u8) = (0x{r:02X}, 0x{g:02X}, 0x{b:02X});\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
let dest = Path::new(&env::var("OUT_DIR").expect("OUT_DIR")).join("tokens.rs");
|
||||
fs::write(&dest, out).expect("write tokens.rs");
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
/// The declarations of the theme's block: from the selector naming it (at the start of a line,
|
||||
/// so a mention in a comment does not count) to the block's closing brace at the start of a
|
||||
/// line, which is the file's own layout, one block per theme.
|
||||
fn theme_block<'a>(css: &'a str, theme: &str) -> Option<&'a str> {
|
||||
let selector = format!("\n:root[data-theme=\"{theme}\"]");
|
||||
let at = css.find(&selector)?;
|
||||
let open = at + css[at..].find('{')?;
|
||||
let close = open + css[open..].find("\n}")?;
|
||||
Some(&css[open + 1..close])
|
||||
}
|
||||
|
||||
/// `--token: #RRGGBB;` in the block, as bytes.
|
||||
fn hex_token(block: &str, token: &str) -> Option<(u8, u8, u8)> {
|
||||
let value = block
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find_map(|l| {
|
||||
l.strip_prefix(token)
|
||||
.and_then(|r| r.trim_start().strip_prefix(':'))
|
||||
})?
|
||||
.trim()
|
||||
.trim_end_matches(';')
|
||||
.trim();
|
||||
let hex = value.strip_prefix('#')?;
|
||||
if hex.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok();
|
||||
Some((byte(0)?, byte(2)?, byte(4)?))
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "The app's own pages: the title bar and the first-run page. The server's web UI is a remote page in the same window and gets no IPC at all: this capability lists no remote URL. The window permissions are the title bar's: moving, resizing, minimising, maximising and closing (which hides).",
|
||||
"windows": ["main"],
|
||||
"description": "The app's own page (ui/), the only content with the bridge. It names the main window and no remote URL: no server page is ever loaded, and HTML the viewer shows is a sealed frame on its own scheme, which no capability names. The window permissions are the title bar's: moving, resizing, minimising, maximising and closing (which hides).",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
|
||||
@@ -108,6 +108,25 @@ fn running_in(state: &Path) -> Option<u32> {
|
||||
(pid != 0 && alive(pid)).then_some(pid)
|
||||
}
|
||||
|
||||
/// This computer's host name: what the CLI sent the server as `device_host` at login
|
||||
/// (`os.hostname()`, the kernel's node name), so the server can tell which device is this one.
|
||||
#[cfg(unix)]
|
||||
pub fn hostname() -> Option<String> {
|
||||
let mut buf = [0u8; 256];
|
||||
let r = unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) };
|
||||
if r != 0 {
|
||||
return None;
|
||||
}
|
||||
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
||||
let name = String::from_utf8_lossy(&buf[..end]).trim().to_string();
|
||||
(!name.is_empty()).then_some(name)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn hostname() -> Option<String> {
|
||||
std::env::var("COMPUTERNAME").ok().filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn alive(pid: u32) -> bool {
|
||||
// Signal 0 checks without sending; EPERM means it exists but is someone else's.
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//! The agent's working directory, for the explorer and the viewer: list a folder, read a file.
|
||||
//!
|
||||
//! Only inside the folders the agent may work in (`lembas remote` roots, as the CLI last said):
|
||||
//! the page names a folder and a path inside it, and both are resolved here -- symbolic links and
|
||||
//! `..` included -- before anything is opened. A path that ends up outside every root is refused,
|
||||
//! whatever the page asked. Reading only: the app never writes in a project, the agent does.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// The most of a folder listed at once.
|
||||
pub const MAX_ENTRIES: usize = 5000;
|
||||
/// The most of a file read for the viewer; a bigger one is offered by name only.
|
||||
pub const MAX_READ: u64 = 2 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
/// From the folder the page named, with `/` between parts.
|
||||
pub path: String,
|
||||
/// `dir` or `file` (a link counts as what it points to; a broken one as a file).
|
||||
pub kind: &'static str,
|
||||
pub size: u64,
|
||||
/// Seconds since 1970, when the system says.
|
||||
pub modified: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct Listing {
|
||||
pub root: String,
|
||||
pub path: String,
|
||||
pub entries: Vec<Entry>,
|
||||
/// More entries than `MAX_ENTRIES`: the rest are not listed.
|
||||
pub more: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct Opened {
|
||||
pub root: String,
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
/// The text, when the file is text and small enough.
|
||||
pub text: Option<String>,
|
||||
/// Why there is no text: `binary` or `too_big`.
|
||||
pub withheld: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// `root` (a folder the agent may use, or one inside it) and `path` inside it, resolved, and
|
||||
/// refused unless the result is inside one of `roots`. The canonical root and target.
|
||||
pub fn resolve(roots: &[String], root: &str, path: &str) -> Result<(PathBuf, PathBuf), String> {
|
||||
let base = fs::canonicalize(root).map_err(|e| format!("{root}: {e}"))?;
|
||||
let allowed = roots
|
||||
.iter()
|
||||
.filter_map(|r| fs::canonicalize(r).ok())
|
||||
.any(|r| base.starts_with(&r));
|
||||
if !allowed {
|
||||
return Err(format!(
|
||||
"{root} is not a folder the agent may work in. Add it under Settings, Project folders."
|
||||
));
|
||||
}
|
||||
let rel = Path::new(path.trim_start_matches('/'));
|
||||
if rel.components().any(|c| matches!(c, Component::Prefix(_))) {
|
||||
return Err(format!("Not a path inside {root}: {path}"));
|
||||
}
|
||||
let target = fs::canonicalize(base.join(rel)).map_err(|e| format!("{path}: {e}"))?;
|
||||
if !target.starts_with(&base) {
|
||||
return Err(format!("{path} is outside {root}."));
|
||||
}
|
||||
Ok((base, target))
|
||||
}
|
||||
|
||||
/// `target` from `base`, with `/` between parts ("" for the base itself).
|
||||
fn relative(base: &Path, target: &Path) -> String {
|
||||
target
|
||||
.strip_prefix(base)
|
||||
.map(|p| {
|
||||
p.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn list(roots: &[String], root: &str, path: &str) -> Result<Listing, String> {
|
||||
let (base, dir) = resolve(roots, root, path)?;
|
||||
if !dir.is_dir() {
|
||||
return Err(format!("{path} is not a folder."));
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
let mut more = false;
|
||||
for item in fs::read_dir(&dir).map_err(|e| format!("{path}: {e}"))? {
|
||||
let Ok(item) = item else { continue };
|
||||
if entries.len() >= MAX_ENTRIES {
|
||||
more = true;
|
||||
break;
|
||||
}
|
||||
let name = item.file_name().to_string_lossy().into_owned();
|
||||
// A link is shown as what it points to; following it is `resolve`'s to allow or not.
|
||||
let meta = fs::metadata(item.path()).or_else(|_| item.metadata());
|
||||
let (kind, size, modified) = match meta {
|
||||
Ok(m) => (
|
||||
if m.is_dir() { "dir" } else { "file" },
|
||||
if m.is_dir() { 0 } else { m.len() },
|
||||
m.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs()),
|
||||
),
|
||||
Err(_) => ("file", 0, None),
|
||||
};
|
||||
let at = relative(&base, &dir);
|
||||
entries.push(Entry {
|
||||
path: if at.is_empty() {
|
||||
name.clone()
|
||||
} else {
|
||||
format!("{at}/{name}")
|
||||
},
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
modified,
|
||||
});
|
||||
}
|
||||
entries.sort_by(|a, b| {
|
||||
(a.kind != "dir")
|
||||
.cmp(&(b.kind != "dir"))
|
||||
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
Ok(Listing {
|
||||
root: base.to_string_lossy().into_owned(),
|
||||
path: relative(&base, &dir),
|
||||
entries,
|
||||
more,
|
||||
})
|
||||
}
|
||||
|
||||
/// Text, as the CLI's `show` decides it: no NUL byte, and valid UTF-8.
|
||||
fn as_text(data: Vec<u8>) -> Option<String> {
|
||||
if data.contains(&0) {
|
||||
return None;
|
||||
}
|
||||
String::from_utf8(data).ok()
|
||||
}
|
||||
|
||||
pub fn read(roots: &[String], root: &str, path: &str) -> Result<Opened, String> {
|
||||
let (base, file) = resolve(roots, root, path)?;
|
||||
let meta = fs::metadata(&file).map_err(|e| format!("{path}: {e}"))?;
|
||||
if !meta.is_file() {
|
||||
return Err(format!("{path} is not a file."));
|
||||
}
|
||||
let name = file
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let mut opened = Opened {
|
||||
root: base.to_string_lossy().into_owned(),
|
||||
path: relative(&base, &file),
|
||||
name,
|
||||
size: meta.len(),
|
||||
text: None,
|
||||
withheld: None,
|
||||
};
|
||||
if meta.len() > MAX_READ {
|
||||
opened.withheld = Some("too_big");
|
||||
return Ok(opened);
|
||||
}
|
||||
let data = fs::read(&file).map_err(|e| format!("{path}: {e}"))?;
|
||||
match as_text(data) {
|
||||
Some(t) => opened.text = Some(t),
|
||||
None => opened.withheld = Some("binary"),
|
||||
}
|
||||
Ok(opened)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("llembas-files-{name}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(d.join("project/src")).unwrap();
|
||||
fs::create_dir_all(d.join("outside")).unwrap();
|
||||
fs::write(d.join("project/README.md"), "# Waybread\n").unwrap();
|
||||
fs::write(d.join("project/src/main.rs"), "fn main() {}\n").unwrap();
|
||||
fs::write(d.join("project/blob.bin"), [0u8, 1, 2]).unwrap();
|
||||
fs::write(d.join("outside/secret.txt"), "no").unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn roots(d: &Path) -> Vec<String> {
|
||||
vec![d.join("project").to_string_lossy().into_owned()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_root_lists_folders_first() {
|
||||
let d = scratch("list");
|
||||
let root = d.join("project").to_string_lossy().into_owned();
|
||||
let l = list(&roots(&d), &root, "").unwrap();
|
||||
let names: Vec<_> = l.entries.iter().map(|e| e.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["src", "blob.bin", "README.md"]);
|
||||
assert_eq!(l.entries[0].kind, "dir");
|
||||
let inner = list(&roots(&d), &root, "src").unwrap();
|
||||
assert_eq!(inner.entries[0].path, "src/main.rs");
|
||||
fs::remove_dir_all(&d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_outside_the_roots_is_reached() {
|
||||
let d = scratch("outside");
|
||||
let root = d.join("project").to_string_lossy().into_owned();
|
||||
let other = d.join("outside").to_string_lossy().into_owned();
|
||||
assert!(list(&roots(&d), &other, "").is_err());
|
||||
assert!(read(&roots(&d), &root, "../outside/secret.txt").is_err());
|
||||
assert!(read(&roots(&d), &root, "/../outside/secret.txt").is_err());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(d.join("outside"), d.join("project/escape")).unwrap();
|
||||
assert!(read(&roots(&d), &root, "escape/secret.txt").is_err());
|
||||
// A folder inside a root is a root of its own for the pane (an agent chat's folder).
|
||||
let sub = d.join("project/src").to_string_lossy().into_owned();
|
||||
assert!(list(&roots(&d), &sub, "").is_ok());
|
||||
}
|
||||
fs::remove_dir_all(&d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_is_read_and_the_rest_is_withheld() {
|
||||
let d = scratch("read");
|
||||
let root = d.join("project").to_string_lossy().into_owned();
|
||||
let md = read(&roots(&d), &root, "README.md").unwrap();
|
||||
assert_eq!(md.text.as_deref(), Some("# Waybread\n"));
|
||||
assert_eq!(md.path, "README.md");
|
||||
let bin = read(&roots(&d), &root, "blob.bin").unwrap();
|
||||
assert_eq!(bin.text, None);
|
||||
assert_eq!(bin.withheld, Some("binary"));
|
||||
assert!(read(&roots(&d), &root, "src").is_err());
|
||||
fs::remove_dir_all(&d).unwrap();
|
||||
}
|
||||
}
|
||||
+1070
-239
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,77 @@
|
||||
//! Which folders the agent may work in: the CLI's `remote` settings, read and changed through
|
||||
//! `lembas remote … --json` so the app never edits the CLI's files itself. A freshly linked
|
||||
//! computer allows no folder at all (remote control is off and `roots` is empty), so the agent
|
||||
//! is connected but every agent chat on it is refused until at least one folder is added.
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::cli;
|
||||
|
||||
/// `lembas remote list --json`, and what every changing command answers with.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct Remote {
|
||||
pub enabled: bool,
|
||||
pub roots: Vec<String>,
|
||||
pub max_mode: String,
|
||||
pub terminal: bool,
|
||||
/// The running service must start again for the change to apply.
|
||||
pub restart_needed: bool,
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
/// Agent chats can be opened here at all.
|
||||
pub fn usable(&self) -> bool {
|
||||
self.enabled && !self.roots.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `lembas remote <args…> --json`: the new state, or what the CLI said went wrong.
|
||||
pub fn run(args: &[&str]) -> Result<Remote, String> {
|
||||
let out = Command::new(cli::binary())
|
||||
.arg("remote")
|
||||
.args(args)
|
||||
.arg("--json")
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.map_err(|e| format!("Could not start {}: {e}", cli::binary().display()))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
return Err(if err.is_empty() {
|
||||
format!("lembas remote {} failed ({})", args.join(" "), out.status)
|
||||
} else {
|
||||
err.trim_start_matches("lembas: ").to_string()
|
||||
});
|
||||
}
|
||||
parse(&String::from_utf8_lossy(&out.stdout))
|
||||
}
|
||||
|
||||
/// The last JSON object the CLI printed (it may say a word before it).
|
||||
fn parse(stdout: &str) -> Result<Remote, String> {
|
||||
stdout
|
||||
.lines()
|
||||
.rev()
|
||||
.find_map(|l| serde_json::from_str::<Remote>(l.trim()).ok())
|
||||
.ok_or_else(|| "lembas remote gave no answer this version of the app understands — is the bundled LLeMbas CLI too old?".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reads_the_state_and_defaults_what_is_missing() {
|
||||
let r = parse(
|
||||
"added /home/a/project\n{\"enabled\":true,\"roots\":[\"/home/a/project\"],\"max_mode\":\"edit\",\"terminal\":false}\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.usable());
|
||||
assert_eq!(r.roots, vec!["/home/a/project"]);
|
||||
assert!(!r.restart_needed);
|
||||
let off = parse("{\"enabled\":false,\"roots\":[]}").unwrap();
|
||||
assert!(!off.usable());
|
||||
assert!(parse("lembas: unknown command remote").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! HTML to look at, sealed: an HTML file in the viewer, whether it is the agent's (`show`), the
|
||||
//! server's tree's or this computer's, is a page a model may have written. It is drawn in a frame
|
||||
//! from an address of its own (`llembas-sealed://`), never as part of the app's page:
|
||||
//!
|
||||
//! - **No bridge.** The capability names only the app's own page; Tauri gives the IPC script to
|
||||
//! the main frame only, and this scheme is not the app's origin. Nothing in the frame can call
|
||||
//! the app.
|
||||
//! - **No network, no app.** It is served under the server's sealed policy (its
|
||||
//! `security/sealed.py`): `sandbox allow-scripts` without `allow-same-origin` makes it an opaque
|
||||
//! origin, nothing loads from anywhere (inline and `data:`/`blob:` only), no form is sent.
|
||||
//! - **No way out.** The app refuses every navigation it did not make (see `lib.rs`), so a link or
|
||||
//! a script in the frame goes nowhere.
|
||||
//!
|
||||
//! The page holds the HTML until it is closed (`forget`); at most `KEEP` are kept.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub const SCHEME: &str = "llembas-sealed";
|
||||
/// The most sealed pages held at once; the oldest goes first.
|
||||
const KEEP: usize = 16;
|
||||
|
||||
/// The policy a sealed page is served under -- the server's `sealed.policy()`, minus
|
||||
/// `frame-ancestors`, which would have to name the app's own scheme.
|
||||
pub const POLICY: &str = "sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' data: blob:; style-src 'unsafe-inline' data: blob:; img-src data: blob:; font-src data: blob:; media-src data: blob:; connect-src 'none'; worker-src blob:; frame-src 'none'; object-src 'none'; form-action 'none'; base-uri 'none'";
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Pages {
|
||||
next: Mutex<u64>,
|
||||
held: Mutex<VecDeque<(String, String)>>,
|
||||
}
|
||||
|
||||
impl Pages {
|
||||
/// Hold `html`; the id it is served under.
|
||||
pub fn hold(&self, html: String) -> String {
|
||||
let id = {
|
||||
let mut n = self.next.lock().unwrap();
|
||||
*n += 1;
|
||||
format!("p{}", *n)
|
||||
};
|
||||
let mut held = self.held.lock().unwrap();
|
||||
while held.len() >= KEEP {
|
||||
held.pop_front();
|
||||
}
|
||||
held.push_back((id.clone(), html));
|
||||
id
|
||||
}
|
||||
|
||||
pub fn forget(&self, id: &str) {
|
||||
self.held.lock().unwrap().retain(|(i, _)| i != id);
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str) -> Option<String> {
|
||||
self.held
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|(i, _)| i == id)
|
||||
.map(|(_, h)| h.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a held page is served: the address form differs by platform, as Tauri's own does.
|
||||
pub fn url(id: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
format!("http://{SCHEME}.localhost/{id}")
|
||||
} else {
|
||||
format!("{SCHEME}://localhost/{id}")
|
||||
}
|
||||
}
|
||||
|
||||
/// The id in a request's path: `/p3` -> `p3`.
|
||||
pub fn id_of(path: &str) -> &str {
|
||||
path.trim_start_matches('/')
|
||||
.split(['?', '/'])
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
/// The response for a request to the scheme.
|
||||
pub fn respond(pages: &Pages, path: &str) -> tauri::http::Response<Vec<u8>> {
|
||||
let builder = tauri::http::Response::builder()
|
||||
.header("Content-Security-Policy", POLICY)
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.header("Referrer-Policy", "no-referrer")
|
||||
.header("Cache-Control", "no-store");
|
||||
let built = match pages.get(id_of(path)) {
|
||||
Some(html) => builder
|
||||
.status(200)
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(html.into_bytes()),
|
||||
None => builder
|
||||
.status(404)
|
||||
.header("Content-Type", "text/plain; charset=utf-8")
|
||||
.body(Vec::new()),
|
||||
};
|
||||
built.unwrap_or_else(|_| tauri::http::Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pages_are_held_by_id_and_the_oldest_goes() {
|
||||
let p = Pages::default();
|
||||
let first = p.hold("<p>one</p>".into());
|
||||
assert_eq!(p.get(&first).as_deref(), Some("<p>one</p>"));
|
||||
for i in 0..KEEP {
|
||||
p.hold(format!("<p>{i}</p>"));
|
||||
}
|
||||
assert_eq!(p.get(&first), None);
|
||||
let last = p.hold("<p>last</p>".into());
|
||||
p.forget(&last);
|
||||
assert_eq!(p.get(&last), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sealed_page_is_served_sealed() {
|
||||
let p = Pages::default();
|
||||
let id = p.hold("<script>alert(1)</script>".into());
|
||||
let res = respond(&p, &format!("/{id}"));
|
||||
assert_eq!(res.status(), 200);
|
||||
let csp = res.headers()["Content-Security-Policy"].to_str().unwrap();
|
||||
assert!(csp.starts_with("sandbox allow-scripts"));
|
||||
assert!(!csp.contains("allow-same-origin"));
|
||||
assert!(csp.contains("connect-src 'none'"));
|
||||
assert_eq!(respond(&p, "/nope").status(), 404);
|
||||
assert_eq!(id_of("/p7?x=1"), "p7");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,772 @@
|
||||
//! The server this app is a front end to: whether it is one this app can talk to, the app's own
|
||||
//! sign-in to it, and every request the app's pages make to it.
|
||||
//!
|
||||
//! - **Discovery** (`/.well-known/lembas.json`) decides first. LLeMbas Desktop 1.0 needs a
|
||||
//! LLeMbas 2.0 server: one that speaks link protocol 3 and has the app's door (`/api/v2`) in
|
||||
//! version 2. That is checked by `app_api`, not by the server's version number, which says 2
|
||||
//! only from its first 2.0 release on. An older server is refused with a sentence that says
|
||||
//! which side to update, before anything is signed in.
|
||||
//! - **The app's sign-in** is the server's device login (RFC 8628), the same one `lembas login`
|
||||
//! uses, for `app models`: "read and write your chats, as the web interface does", and the
|
||||
//! speech routes. **One approval, two tokens**: where discovery says
|
||||
//! `login.device.agent_scope`, the same login asks for the agent's scopes too
|
||||
//! (`models memory link`), and the approval answers with a second token, the agent's, which is
|
||||
//! handed straight to the bundled CLI (`lembas login --token-stdin`) and never kept here. The
|
||||
//! approval page opens in the system's browser, where the person is signed in already. The
|
||||
//! app's token is kept in the app's config directory, readable by this user only.
|
||||
//! - **The token stays in Rust.** The pages ask for `/api/v2/...` through a command and get the
|
||||
//! JSON back; they never hold the token, so no page -- and nothing a page shows -- can send it
|
||||
//! anywhere. A request can only reach the server's `/api/v2`: the path is checked here.
|
||||
//!
|
||||
//! The agent's own link is a different token, the CLI's, which this app never keeps or reads
|
||||
//! back: a token that may accept work as a device is not one the chat window needs.
|
||||
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// The link protocol a 2.0 server speaks (harness spec `acp.md`, *Versions*).
|
||||
pub const NEEDS_PROTOCOL: i64 = 3;
|
||||
/// The `/api/v2` contract this app is written against: discovery's `app_api`. A lower number is
|
||||
/// a server before 2.0 (its chats carry what this app no longer reads, and it has no `show`
|
||||
/// card); a higher one is a change an existing client would misread, so this app says it is too
|
||||
/// old rather than guess.
|
||||
pub const APP_API: i64 = 2;
|
||||
/// The scopes the app's own token asks for: the chats, and the speech routes of `/v1`.
|
||||
pub const APP_SCOPE: &str = "app models";
|
||||
/// The scopes the agent's token asks for, in the same approval: what `lembas service` needs.
|
||||
pub const AGENT_SCOPE: &str = "models memory link";
|
||||
|
||||
/// What discovery said, once it has passed every check.
|
||||
#[derive(Clone, Debug, Serialize, PartialEq)]
|
||||
pub struct Discovery {
|
||||
pub base: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
/// The app's door: `https://host/api/v2`.
|
||||
#[serde(skip)]
|
||||
pub app: String,
|
||||
#[serde(skip)]
|
||||
pub device_code: String,
|
||||
#[serde(skip)]
|
||||
pub device_token: String,
|
||||
/// The server gives the agent's token from the app's own approval
|
||||
/// (`login.device.agent_scope`): one approval, not two.
|
||||
pub one_approval: bool,
|
||||
}
|
||||
|
||||
fn agent(read: Duration) -> ureq::Agent {
|
||||
ureq::AgentBuilder::new()
|
||||
.timeout_connect(Duration::from_secs(10))
|
||||
.timeout_read(read)
|
||||
.timeout_write(Duration::from_secs(30))
|
||||
.user_agent(&format!("llembas-desktop/{}", env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
}
|
||||
|
||||
/// An error a page can show and act on: the HTTP status (0: the server was not reached) and the
|
||||
/// server's own sentence (`detail`), or ours.
|
||||
#[derive(Clone, Debug, Serialize, PartialEq)]
|
||||
pub struct ApiError {
|
||||
pub status: u16,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn new(status: u16, detail: impl Into<String>) -> Self {
|
||||
ApiError {
|
||||
status,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ureq::Error> for ApiError {
|
||||
fn from(e: ureq::Error) -> Self {
|
||||
match e {
|
||||
ureq::Error::Status(code, res) => {
|
||||
let text = res.into_string().unwrap_or_default();
|
||||
let detail = serde_json::from_str::<Value>(&text)
|
||||
.ok()
|
||||
.and_then(|v| v.get("detail").cloned())
|
||||
.map(|d| match d {
|
||||
Value::String(s) => s,
|
||||
other => other.to_string(),
|
||||
})
|
||||
.unwrap_or_else(|| format!("The server answered {code}."));
|
||||
ApiError::new(code, detail)
|
||||
}
|
||||
ureq::Error::Transport(t) => {
|
||||
ApiError::new(0, format!("Could not reach the server: {t}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask `base` who it is, and refuse what this app cannot talk to.
|
||||
pub fn discover(base: &str) -> Result<Discovery, String> {
|
||||
let url = format!("{base}/.well-known/lembas.json");
|
||||
let res = agent(Duration::from_secs(15))
|
||||
.get(&url)
|
||||
.call()
|
||||
.map_err(|e| match e {
|
||||
ureq::Error::Status(404, _) => format!("{base} answers, but is not a LLeMbas server."),
|
||||
ureq::Error::Status(code, _) => format!("{base} answered {code}."),
|
||||
ureq::Error::Transport(t) => format!("Could not reach {base}: {t}"),
|
||||
})?;
|
||||
let body: Value = res
|
||||
.into_json()
|
||||
.map_err(|_| format!("{base} is not a LLeMbas server."))?;
|
||||
check(&body, base)
|
||||
}
|
||||
|
||||
/// The checks, on discovery's answer: a LLeMbas server, a 2.0 one (protocol 3, and the app
|
||||
/// door in version 2), with an app door whose version this app knows. The server's version
|
||||
/// number is not checked: a 2.0 server in the making still says 1.7 until its first beta.
|
||||
pub fn check(body: &Value, base: &str) -> Result<Discovery, String> {
|
||||
if body.get("service").and_then(Value::as_str) != Some("lembas") {
|
||||
return Err(format!("{base} is not a LLeMbas server."));
|
||||
}
|
||||
let version = body
|
||||
.get("version")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
// `protocols` is a 2.0 server's list; a 1.x one has only `protocol`, frozen at 1.
|
||||
let protocols: Vec<i64> = match body.get("protocols").and_then(Value::as_array) {
|
||||
Some(list) => list.iter().filter_map(Value::as_i64).collect(),
|
||||
None => body
|
||||
.get("protocol")
|
||||
.and_then(Value::as_i64)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
let app_api = body.get("app_api").and_then(Value::as_i64).unwrap_or(0);
|
||||
let new_enough = protocols.contains(&NEEDS_PROTOCOL) && app_api >= APP_API;
|
||||
if !new_enough {
|
||||
let runs = if version.is_empty() {
|
||||
"an older LLeMbas".to_string()
|
||||
} else {
|
||||
format!("LLeMbas {version}")
|
||||
};
|
||||
return Err(format!(
|
||||
"{base} runs {runs}. LLeMbas Desktop {} needs a LLeMbas 2.0 server or later: update the server, or keep using LLeMbas Desktop 0.7 with this one.",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
}
|
||||
if app_api > APP_API {
|
||||
return Err(format!(
|
||||
"{base} speaks a newer app interface (version {app_api}) than LLeMbas Desktop {} knows (version {APP_API}). Update LLeMbas Desktop.",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
}
|
||||
let api = body.get("api");
|
||||
let app = api
|
||||
.and_then(|a| a.get("app"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|s| s.trim_end_matches('/').to_string())
|
||||
.unwrap_or_else(|| format!("{base}/api/v2"));
|
||||
let device = body.get("login").and_then(|l| l.get("device"));
|
||||
let endpoint = |key: &str| {
|
||||
device
|
||||
.and_then(|d| d.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(String::from)
|
||||
};
|
||||
let (Some(device_code), Some(device_token)) = (endpoint("code"), endpoint("token")) else {
|
||||
return Err(format!("{base} offers no device sign-in."));
|
||||
};
|
||||
Ok(Discovery {
|
||||
base: base.to_string(),
|
||||
name: body
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("LLeMbas")
|
||||
.to_string(),
|
||||
version,
|
||||
app,
|
||||
device_code,
|
||||
device_token,
|
||||
one_approval: device
|
||||
.and_then(|d| d.get("agent_scope"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
// --- The app's sign-in ----------------------------------------------------------------------
|
||||
|
||||
/// What the app keeps of its sign-in: which server, the token, and whose it is.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct Account {
|
||||
pub server: String,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub token: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// The same account without its token: what a page may see.
|
||||
pub fn public(&self) -> Account {
|
||||
Account {
|
||||
token: String::new(),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn account_file(config_dir: &Path) -> PathBuf {
|
||||
config_dir.join("account.json")
|
||||
}
|
||||
|
||||
/// The sign-in for `server`, if there is one (a sign-in to another server is not this one's).
|
||||
pub fn load_account(config_dir: &Path, server: &str) -> Option<Account> {
|
||||
let text = fs::read_to_string(account_file(config_dir)).ok()?;
|
||||
let a: Account = serde_json::from_str(&text).ok()?;
|
||||
(a.server == server && !a.token.is_empty()).then_some(a)
|
||||
}
|
||||
|
||||
/// Written to a new file readable by this user only, then moved over the old one.
|
||||
pub fn save_account(config_dir: &Path, a: &Account) -> std::io::Result<()> {
|
||||
fs::create_dir_all(config_dir)?;
|
||||
let tmp = config_dir.join("account.json.tmp");
|
||||
let _ = fs::remove_file(&tmp);
|
||||
let mut options = fs::OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
let mut f = options.open(&tmp)?;
|
||||
f.write_all((serde_json::to_string_pretty(a)? + "\n").as_bytes())?;
|
||||
f.sync_all()?;
|
||||
drop(f);
|
||||
fs::rename(tmp, account_file(config_dir))
|
||||
}
|
||||
|
||||
pub fn forget_account(config_dir: &Path) {
|
||||
let _ = fs::remove_file(account_file(config_dir));
|
||||
}
|
||||
|
||||
/// A device login in progress: the code to approve and where.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Code {
|
||||
pub user_code: String,
|
||||
pub verification_uri: String,
|
||||
pub verification_uri_complete: String,
|
||||
pub expires_in: u64,
|
||||
#[serde(skip)]
|
||||
pub interval: u64,
|
||||
#[serde(skip)]
|
||||
pub device_code: String,
|
||||
}
|
||||
|
||||
/// What a device login asks the server for, as the form it posts.
|
||||
fn code_request(host: &str, with_agent: bool) -> Value {
|
||||
let platform = format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH);
|
||||
let device_name = if host.is_empty() {
|
||||
"LLeMbas Desktop".to_string()
|
||||
} else {
|
||||
format!("LLeMbas Desktop on {host}")
|
||||
};
|
||||
let mut body = serde_json::json!({
|
||||
"client_id": format!("llembas-desktop {}", env!("CARGO_PKG_VERSION")),
|
||||
"device_name": device_name,
|
||||
"device_platform": platform,
|
||||
"scope": APP_SCOPE,
|
||||
});
|
||||
if with_agent {
|
||||
// The agent's token is the device an agent chat runs on: it carries this computer's
|
||||
// host, as `lembas login` sends it, so the app knows which device is this one. The
|
||||
// app's own token alone names no host: it is not a device.
|
||||
body["device_host"] = Value::String(host.to_string());
|
||||
body["agent_scope"] = Value::String(AGENT_SCOPE.to_string());
|
||||
}
|
||||
body
|
||||
}
|
||||
|
||||
/// Open a device login for the app's scopes, and the agent's too when `with_agent` (only where
|
||||
/// discovery offered it).
|
||||
pub fn start_sign_in(d: &Discovery, host: &str, with_agent: bool) -> Result<Code, String> {
|
||||
let res = agent(Duration::from_secs(15))
|
||||
.post(&d.device_code)
|
||||
.set("accept", "application/json")
|
||||
.send_json(code_request(host, with_agent && d.one_approval))
|
||||
.map_err(|e| oauth_error(e, "The server refused to start a sign-in"))?;
|
||||
let v: Value = res
|
||||
.into_json()
|
||||
.map_err(|e| format!("The server's answer was not understood: {e}"))?;
|
||||
let s = |k: &str| v.get(k).and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let n = |k: &str, or: u64| v.get(k).and_then(Value::as_u64).unwrap_or(or);
|
||||
let code = Code {
|
||||
user_code: s("user_code"),
|
||||
verification_uri: s("verification_uri"),
|
||||
verification_uri_complete: match s("verification_uri_complete") {
|
||||
c if c.is_empty() => s("verification_uri"),
|
||||
c => c,
|
||||
},
|
||||
expires_in: n("expires_in", 600),
|
||||
interval: n("interval", 5).max(1),
|
||||
device_code: s("device_code"),
|
||||
};
|
||||
if code.device_code.is_empty() || code.verification_uri.is_empty() {
|
||||
return Err("The server's answer was not understood: no code.".into());
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
/// RFC 8628's error, in words.
|
||||
fn oauth_error(e: ureq::Error, what: &str) -> String {
|
||||
match e {
|
||||
ureq::Error::Status(code, res) => {
|
||||
let v: Value = res.into_json().unwrap_or(Value::Null);
|
||||
let said = v
|
||||
.get("error_description")
|
||||
.or_else(|| v.get("error"))
|
||||
.and_then(Value::as_str)
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| format!("{code}"));
|
||||
format!("{what}: {said}")
|
||||
}
|
||||
ureq::Error::Transport(t) => format!("Could not reach the server: {t}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// What an approval gave: the app's sign-in, and the agent's token when the login asked for one.
|
||||
/// The agent's token is not kept: it goes to the CLI and is dropped.
|
||||
pub struct Granted {
|
||||
pub account: Account,
|
||||
pub agent_token: Option<String>,
|
||||
}
|
||||
|
||||
/// The token endpoint's answer, once it carries a token.
|
||||
fn granted(v: &Value, server: &str) -> Option<Granted> {
|
||||
let token = v.get("access_token").and_then(Value::as_str)?;
|
||||
let account = v.get("account");
|
||||
let field = |k: &str| {
|
||||
account
|
||||
.and_then(|a| a.get(k))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
};
|
||||
Some(Granted {
|
||||
account: Account {
|
||||
server: server.to_string(),
|
||||
token: token.to_string(),
|
||||
email: field("email"),
|
||||
name: field("name"),
|
||||
},
|
||||
agent_token: v
|
||||
.get("agent_token")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|t| !t.is_empty())
|
||||
.map(String::from),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wait for the person to approve the code (or not), checking `cancelled` while waiting.
|
||||
pub fn await_sign_in(
|
||||
d: &Discovery,
|
||||
code: &Code,
|
||||
cancelled: &AtomicBool,
|
||||
) -> Result<Granted, String> {
|
||||
let deadline = Instant::now() + Duration::from_secs(code.expires_in.max(1));
|
||||
let mut interval = Duration::from_secs(code.interval);
|
||||
let http = agent(Duration::from_secs(15));
|
||||
while Instant::now() < deadline {
|
||||
let until = Instant::now() + interval;
|
||||
while Instant::now() < until {
|
||||
if cancelled.load(Ordering::SeqCst) {
|
||||
return Err("The sign-in was cancelled.".into());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
let res = http
|
||||
.post(&d.device_token)
|
||||
.set("accept", "application/json")
|
||||
.send_json(serde_json::json!({
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||
"device_code": code.device_code,
|
||||
}));
|
||||
let v: Value = match res {
|
||||
Ok(r) => r.into_json().unwrap_or(Value::Null),
|
||||
Err(ureq::Error::Status(_, r)) => r.into_json().unwrap_or(Value::Null),
|
||||
Err(ureq::Error::Transport(t)) => {
|
||||
return Err(format!("Could not reach the server: {t}"))
|
||||
}
|
||||
};
|
||||
if let Some(g) = granted(&v, &d.base) {
|
||||
return Ok(g);
|
||||
}
|
||||
match v.get("error").and_then(Value::as_str) {
|
||||
Some("authorization_pending") => {}
|
||||
Some("slow_down") => interval += Duration::from_secs(5),
|
||||
Some("access_denied") => return Err("The sign-in was denied on the server.".into()),
|
||||
Some("expired_token") => {
|
||||
return Err("The code expired before it was approved. Sign in again.".into())
|
||||
}
|
||||
Some(other) => return Err(format!("The server refused the sign-in: {other}")),
|
||||
None => return Err("The server's answer was not understood.".into()),
|
||||
}
|
||||
}
|
||||
Err("The code expired before it was approved. Sign in again.".into())
|
||||
}
|
||||
|
||||
/// Give a token up on the server (the app's: gone from the account's apps then; the agent's:
|
||||
/// one the CLI did not take). Best effort: the app forgets it either way.
|
||||
pub fn sign_out(base: &str, token: &str) {
|
||||
let _ = agent(Duration::from_secs(10))
|
||||
.delete(&format!("{base}/api/v1/token"))
|
||||
.set("authorization", &format!("Bearer {token}"))
|
||||
.call();
|
||||
}
|
||||
|
||||
// --- The app's door -------------------------------------------------------------------------
|
||||
|
||||
/// A path a page may ask for: on the app's door, and nothing that could leave it.
|
||||
pub fn checked_path(path: &str) -> Result<&str, ApiError> {
|
||||
let ok = path.starts_with('/')
|
||||
&& !path.starts_with("//")
|
||||
&& !path.contains("://")
|
||||
&& !path.contains('#')
|
||||
&& !path.contains('\\')
|
||||
&& !path.split(['/', '?']).any(|part| part == "..")
|
||||
&& path.chars().all(|c| !c.is_control() && c != ' ');
|
||||
if ok {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(ApiError::new(
|
||||
400,
|
||||
format!("Not a path on the server: {path}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// One request to `/api/v2`, as the signed-in account. The JSON answer, or `null` for none.
|
||||
pub fn request(
|
||||
app: &str,
|
||||
token: &str,
|
||||
method: &str,
|
||||
path: &str,
|
||||
body: Option<&Value>,
|
||||
) -> Result<Value, ApiError> {
|
||||
let path = checked_path(path)?;
|
||||
let method = method.to_ascii_uppercase();
|
||||
if !["GET", "POST", "PATCH", "PUT", "DELETE"].contains(&method.as_str()) {
|
||||
return Err(ApiError::new(400, format!("No such method: {method}")));
|
||||
}
|
||||
let req = agent(Duration::from_secs(60))
|
||||
.request(&method, &format!("{app}{path}"))
|
||||
.set("authorization", &format!("Bearer {token}"))
|
||||
.set("accept", "application/json");
|
||||
let res = match body {
|
||||
Some(b) => req.send_json(b),
|
||||
None => req.call(),
|
||||
}?;
|
||||
if res.status() == 204 {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let text = res
|
||||
.into_string()
|
||||
.map_err(|e| ApiError::new(0, format!("The server's answer could not be read: {e}")))?;
|
||||
if text.trim().is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|_| ApiError::new(0, "The server's answer was not JSON.".to_string()))
|
||||
}
|
||||
|
||||
/// The most a plain-text answer may be: the server's own limit for a shown file (1 MB), and a
|
||||
/// little room. Anything longer is not read to the end.
|
||||
pub const TEXT_LIMIT: u64 = 1024 * 1024 + 4096;
|
||||
|
||||
/// One `GET` on `/api/v2` that answers plain text, not JSON: the file a `show` card names
|
||||
/// (`/chats/{chat}/show?…`). An error is still the server's JSON `detail` -- 404 when the file
|
||||
/// is no longer the chat's to show, 413 when it is too large, 415 when it is not text.
|
||||
pub fn request_text(app: &str, token: &str, path: &str) -> Result<String, ApiError> {
|
||||
let path = checked_path(path)?;
|
||||
let res = agent(Duration::from_secs(60))
|
||||
.get(&format!("{app}{path}"))
|
||||
.set("authorization", &format!("Bearer {token}"))
|
||||
.set("accept", "text/plain")
|
||||
.call()?;
|
||||
let mut text = String::new();
|
||||
res.into_reader()
|
||||
.take(TEXT_LIMIT)
|
||||
.read_to_string(&mut text)
|
||||
.map_err(|e| ApiError::new(0, format!("The server's answer could not be read: {e}")))?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
/// Server-sent events, a line at a time: what `stream` hands on, one `(event, data)` per frame.
|
||||
#[derive(Default)]
|
||||
pub struct Sse {
|
||||
event: String,
|
||||
data: Vec<String>,
|
||||
}
|
||||
|
||||
impl Sse {
|
||||
/// Feed one line (without its newline); a blank line ends a frame and returns it.
|
||||
pub fn line(&mut self, line: &str) -> Option<(String, String)> {
|
||||
let line = line.strip_suffix('\r').unwrap_or(line);
|
||||
if line.is_empty() {
|
||||
if self.data.is_empty() && self.event.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let event = std::mem::take(&mut self.event);
|
||||
let data = std::mem::take(&mut self.data).join("\n");
|
||||
return Some((
|
||||
if event.is_empty() {
|
||||
"message".into()
|
||||
} else {
|
||||
event
|
||||
},
|
||||
data,
|
||||
));
|
||||
}
|
||||
if line.starts_with(':') {
|
||||
return None; // a keepalive
|
||||
}
|
||||
let (field, value) = match line.split_once(':') {
|
||||
Some((f, v)) => (f, v.strip_prefix(' ').unwrap_or(v)),
|
||||
None => (line, ""),
|
||||
};
|
||||
match field {
|
||||
"event" => self.event = value.to_string(),
|
||||
"data" => self.data.push(value.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow a reply as it is written (`GET …/stream`), handing each frame to `frame` until the
|
||||
/// server closes it, `frame` says stop (false), or `stop` is set. Keepalives arrive often enough
|
||||
/// that `stop` is noticed within seconds.
|
||||
pub fn stream(
|
||||
app: &str,
|
||||
token: &str,
|
||||
path: &str,
|
||||
stop: &AtomicBool,
|
||||
mut frame: impl FnMut(&str, &str) -> bool,
|
||||
) -> Result<(), ApiError> {
|
||||
let path = checked_path(path)?;
|
||||
let res = agent(Duration::from_secs(90))
|
||||
.get(&format!("{app}{path}"))
|
||||
.set("authorization", &format!("Bearer {token}"))
|
||||
.set("accept", "text/event-stream")
|
||||
.call()?;
|
||||
let mut sse = Sse::default();
|
||||
for line in BufReader::new(res.into_reader()).lines() {
|
||||
if stop.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
let line = line.map_err(|e| ApiError::new(0, format!("The stream broke off: {e}")))?;
|
||||
if let Some((event, data)) = sse.line(&line) {
|
||||
if !frame(&event, &data) || event == "close" {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn two_oh() -> Value {
|
||||
json!({
|
||||
"service": "lembas",
|
||||
"version": "2.0.0-beta.1",
|
||||
"name": "Example",
|
||||
"protocol": 3,
|
||||
"protocols": [3],
|
||||
"app_api": 2,
|
||||
"api": {"app": "https://ai.example/api/v2"},
|
||||
"login": {"device": {
|
||||
"code": "https://ai.example/api/device/code",
|
||||
"token": "https://ai.example/api/device/token",
|
||||
"agent_scope": true
|
||||
}}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_2_0_server_passes() {
|
||||
let d = check(&two_oh(), "https://ai.example").unwrap();
|
||||
assert_eq!(d.app, "https://ai.example/api/v2");
|
||||
assert_eq!(d.name, "Example");
|
||||
assert_eq!(d.device_token, "https://ai.example/api/device/token");
|
||||
assert!(d.one_approval);
|
||||
// A server in the making still says 1.7 until its first 2.0 beta: `app_api` decides.
|
||||
let mut dev = two_oh();
|
||||
dev["version"] = json!("1.7.2");
|
||||
assert!(check(&dev, "https://ai.example").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_agent_scope_in_discovery_there_are_two_approvals() {
|
||||
let mut older = two_oh();
|
||||
older["login"]["device"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("agent_scope");
|
||||
assert!(!check(&older, "https://ai.example").unwrap().one_approval);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_approval_asks_for_both_tokens_and_only_then_names_the_host() {
|
||||
let both = code_request("workbench", true);
|
||||
assert_eq!(both["scope"], "app models");
|
||||
assert_eq!(both["agent_scope"], "models memory link");
|
||||
assert_eq!(both["device_host"], "workbench");
|
||||
assert_eq!(both["device_name"], "LLeMbas Desktop on workbench");
|
||||
let app_only = code_request("workbench", false);
|
||||
assert_eq!(app_only["scope"], "app models");
|
||||
assert!(app_only.get("agent_scope").is_none());
|
||||
assert!(app_only.get("device_host").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_approval_gives_the_app_its_token_and_the_agent_its_own() {
|
||||
let v = json!({
|
||||
"access_token": "lmb_app", "token_type": "Bearer", "scope": "app models",
|
||||
"account": {"email": "a@example.org", "name": "A"},
|
||||
"base_url": "https://ai.example",
|
||||
"agent_token": "lmb_agent", "agent_scope": "models memory link"
|
||||
});
|
||||
let g = granted(&v, "https://ai.example").unwrap();
|
||||
assert_eq!(g.account.token, "lmb_app");
|
||||
assert_eq!(g.account.email, "a@example.org");
|
||||
assert_eq!(g.agent_token.as_deref(), Some("lmb_agent"));
|
||||
let one = json!({"access_token": "lmb_app", "account": {}});
|
||||
assert_eq!(
|
||||
granted(&one, "https://ai.example").unwrap().agent_token,
|
||||
None
|
||||
);
|
||||
assert!(granted(&json!({"error": "authorization_pending"}), "x").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_older_server_is_refused_and_told_which_side_to_update() {
|
||||
// 1.x: protocol frozen at 1, no `protocols`, no app door.
|
||||
let old = json!({"service": "lembas", "version": "1.7.3", "protocol": 1});
|
||||
let why = check(&old, "https://ai.example").unwrap_err();
|
||||
assert!(why.contains("LLeMbas 1.7.3"), "{why}");
|
||||
assert!(why.contains("needs a LLeMbas 2.0 server"), "{why}");
|
||||
// Protocol 3 alone is not enough: the app door must be version 2 (1.7 serves 1).
|
||||
let mut early = two_oh();
|
||||
early["version"] = json!("1.7.3");
|
||||
early["app_api"] = json!(1);
|
||||
let why = check(&early, "https://ai.example").unwrap_err();
|
||||
assert!(why.contains("needs a LLeMbas 2.0 server"), "{why}");
|
||||
// The app door without protocol 3 is not one either.
|
||||
let mut odd = two_oh();
|
||||
odd["protocols"] = json!([1, 2]);
|
||||
assert!(check(&odd, "https://ai.example").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_app_interface_this_app_does_not_know_is_refused() {
|
||||
let mut newer = two_oh();
|
||||
newer["app_api"] = json!(APP_API + 1);
|
||||
assert!(check(&newer, "https://ai.example")
|
||||
.unwrap_err()
|
||||
.contains("Update LLeMbas Desktop"));
|
||||
let mut none = two_oh();
|
||||
none.as_object_mut().unwrap().remove("app_api");
|
||||
assert!(check(&none, "https://ai.example").is_err());
|
||||
let not_ours = json!({"service": "other"});
|
||||
assert!(check(¬_ours, "https://ai.example").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_reaches_only_the_app_door() {
|
||||
assert!(checked_path("/chats?kind=agent&limit=50").is_ok());
|
||||
assert!(checked_path("/chats/abc/messages/def/stream").is_ok());
|
||||
for bad in [
|
||||
"chats",
|
||||
"//evil.example/x",
|
||||
"/x://y",
|
||||
"/../v1/admin",
|
||||
"/chats/../../v1",
|
||||
"/a#b",
|
||||
"/a b",
|
||||
"/a\\b",
|
||||
] {
|
||||
assert!(checked_path(bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_sent_events_come_out_whole() {
|
||||
let mut sse = Sse::default();
|
||||
let mut got = vec![];
|
||||
for line in [
|
||||
": keepalive",
|
||||
"",
|
||||
"event: tail",
|
||||
"data: {\"text\":\"a\"}",
|
||||
"",
|
||||
"event: steps",
|
||||
"data: one",
|
||||
"data: two",
|
||||
"\r",
|
||||
"data: plain",
|
||||
"",
|
||||
] {
|
||||
if let Some(f) = sse.line(line) {
|
||||
got.push(f);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
("tail".to_string(), "{\"text\":\"a\"}".to_string()),
|
||||
("steps".to_string(), "one\ntwo".to_string()),
|
||||
("message".to_string(), "plain".to_string()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_account_file_keeps_its_server_and_hides_its_token() {
|
||||
let dir = std::env::temp_dir().join(format!("llembas-account-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
let a = Account {
|
||||
server: "https://ai.example".into(),
|
||||
token: "secret".into(),
|
||||
email: "a@example.org".into(),
|
||||
name: "A".into(),
|
||||
};
|
||||
save_account(&dir, &a).unwrap();
|
||||
assert_eq!(load_account(&dir, "https://ai.example"), Some(a.clone()));
|
||||
assert_eq!(load_account(&dir, "https://other.example"), None);
|
||||
assert!(!serde_json::to_string(&a.public())
|
||||
.unwrap()
|
||||
.contains("secret"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = fs::metadata(account_file(&dir))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode();
|
||||
assert_eq!(mode & 0o777, 0o600);
|
||||
}
|
||||
forget_account(&dir);
|
||||
assert_eq!(load_account(&dir, "https://ai.example"), None);
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,35 @@
|
||||
//! What the app remembers: which server it opens, and whether the agent should run. One small
|
||||
//! JSON file in the app's config directory; nothing secret is in it (the device's key is the
|
||||
//! What the app remembers: which server it talks to, whether the agent should run, the theme and
|
||||
//! whether to notify. One small JSON file in the app's config directory; nothing secret is in it
|
||||
//! (the app's sign-in is `account.json`, readable by this user only; the device's key is the
|
||||
//! LLeMbas CLI's, in its own config).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
/// The server's base address, `https://host[:port]`, with no trailing slash.
|
||||
pub server: Option<String>,
|
||||
/// Run the agent while the app runs (once this computer is linked to the server).
|
||||
pub agent: bool,
|
||||
/// The account's theme the last time the server said (`moria` or `shire`): what the window
|
||||
/// is painted in until it says again. None until it has.
|
||||
pub theme: Option<String>,
|
||||
/// Say with a notification when a reply or an agent's work finishes out of sight.
|
||||
pub notify: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
server: None,
|
||||
agent: false,
|
||||
theme: None,
|
||||
notify: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file(config_dir: &Path) -> PathBuf {
|
||||
@@ -81,9 +98,20 @@ mod tests {
|
||||
let dir = std::env::temp_dir().join(format!("llembas-desktop-test-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
assert_eq!(load(&dir), Settings::default());
|
||||
assert!(Settings::default().notify);
|
||||
// A file from before 1.0 has no `notify`: it is on.
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
fs::write(
|
||||
file(&dir),
|
||||
r#"{"server":"https://ai.example","agent":true}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(load(&dir).notify);
|
||||
let s = Settings {
|
||||
server: Some("https://ai.example".into()),
|
||||
agent: true,
|
||||
theme: Some("shire".into()),
|
||||
notify: false,
|
||||
};
|
||||
save(&dir, &s).unwrap();
|
||||
assert_eq!(load(&dir), s);
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! Native notifications: a reply finished, or an agent chat's work did, while the person was not
|
||||
//! looking at it. The app's own, so the server's Web Push can stay off.
|
||||
//!
|
||||
//! The app asks the server's chat list (`/api/v2/chats`) every `EVERY` and notes which chats are
|
||||
//! being answered; a chat that was and no longer is has finished. The notification is shown
|
||||
//! unless that chat is open in a window in front. Its words come from the page (`set_words`), in
|
||||
//! the page's language; the chat's title is the person's own. Nothing from the reply itself is
|
||||
//! put on screen outside the app.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
pub const EVERY: Duration = Duration::from_secs(15);
|
||||
|
||||
/// What a notification says, in the page's language.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
pub struct Words {
|
||||
/// A chat's reply is ready.
|
||||
pub reply: String,
|
||||
/// An agent chat has finished its work.
|
||||
pub agent: String,
|
||||
}
|
||||
|
||||
impl Default for Words {
|
||||
fn default() -> Self {
|
||||
Words {
|
||||
reply: "The reply is ready.".into(),
|
||||
agent: "The agent has finished.".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A chat that has just finished.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Done {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub agent: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Watch {
|
||||
/// The chat the page shows, if any.
|
||||
pub open: Mutex<Option<String>>,
|
||||
pub words: Mutex<Words>,
|
||||
/// Which chats were being answered at the last look (None before the first look).
|
||||
running: Mutex<Option<HashMap<String, bool>>>,
|
||||
}
|
||||
|
||||
impl Watch {
|
||||
/// Note the chat list as the server gave it; the chats that finished since the last look.
|
||||
/// The first look only notes: what finished before the app looked is not news.
|
||||
pub fn look(&self, list: &Value) -> Vec<Done> {
|
||||
let rows = list
|
||||
.get("chats")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut now = HashMap::new();
|
||||
let mut done = Vec::new();
|
||||
let mut running = self.running.lock().unwrap();
|
||||
for row in &rows {
|
||||
let Some(id) = row.get("id").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let busy = row.get("running").and_then(Value::as_bool).unwrap_or(false);
|
||||
let was = running
|
||||
.as_ref()
|
||||
.and_then(|r| r.get(id).copied())
|
||||
.unwrap_or(false);
|
||||
if was && !busy {
|
||||
done.push(Done {
|
||||
id: id.to_string(),
|
||||
title: row
|
||||
.get("title")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
agent: row.get("kind").and_then(Value::as_str) == Some("agent"),
|
||||
});
|
||||
}
|
||||
now.insert(id.to_string(), busy);
|
||||
}
|
||||
*running = Some(now);
|
||||
done
|
||||
}
|
||||
|
||||
/// Start again from nothing: another server, or signed out.
|
||||
pub fn reset(&self) {
|
||||
*self.running.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn list(rows: &[(&str, bool, &str)]) -> Value {
|
||||
json!({"chats": rows.iter().map(|(id, running, kind)| json!({
|
||||
"id": id, "title": format!("Chat {id}"), "running": running, "kind": kind
|
||||
})).collect::<Vec<_>>()})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chat_that_stops_running_has_finished_and_the_first_look_is_quiet() {
|
||||
let w = Watch::default();
|
||||
assert!(w
|
||||
.look(&list(&[("a", true, "chat"), ("b", false, "agent")]))
|
||||
.is_empty());
|
||||
assert!(w
|
||||
.look(&list(&[("a", true, "chat"), ("b", true, "agent")]))
|
||||
.is_empty());
|
||||
let done = w.look(&list(&[("a", false, "chat"), ("b", false, "agent")]));
|
||||
assert_eq!(
|
||||
done,
|
||||
vec![
|
||||
Done {
|
||||
id: "a".into(),
|
||||
title: "Chat a".into(),
|
||||
agent: false
|
||||
},
|
||||
Done {
|
||||
id: "b".into(),
|
||||
title: "Chat b".into(),
|
||||
agent: true
|
||||
},
|
||||
]
|
||||
);
|
||||
// Said once.
|
||||
assert!(w.look(&list(&[("a", false, "chat")])).is_empty());
|
||||
w.reset();
|
||||
assert!(w.look(&list(&[("a", false, "chat")])).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@
|
||||
"withGlobalTauri": true,
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; connect-src ipc: http://ipc.localhost"
|
||||
"csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src ipc: http://ipc.localhost; frame-src llembas-sealed: http://llembas-sealed.localhost; object-src 'none'; base-uri 'none'; form-action 'none'",
|
||||
"dangerousDisableAssetCspModification": [
|
||||
"style-src"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@@ -20,8 +23,8 @@
|
||||
"appimage"
|
||||
],
|
||||
"category": "Productivity",
|
||||
"shortDescription": "Your LLeMbas server in a window, with the agent on this computer",
|
||||
"longDescription": "LLeMbas Desktop opens your LLeMbas server's web UI in a window of its own and runs the LLeMbas CLI agent on this computer, so agent chats can work in your projects here without a terminal.",
|
||||
"shortDescription": "The LLeMbas agent on this computer, and your server's chats",
|
||||
"longDescription": "LLeMbas Desktop runs the LLeMbas CLI agent on this computer, so agent chats can work in your projects here without a terminal, and shows them, and the chats on your LLeMbas 2.0 server, in an app of its own: the agent's working directory and the server's files side by side, and a viewer for what the agent shows you.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://llembas.eu",
|
||||
"publisher": "LLeMbas",
|
||||
@@ -47,5 +50,14 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXkgMzNBNUJCMkNCQTBCOTEwRgpSV1FQa1F1NkxMdWxNOCtnT2NMQU5QOTBHZ3hUaC9UNHVFZThUR2lqOGdWWUVObi9mbU00amoyago=",
|
||||
"endpoints": [
|
||||
"https://llembas.eu/desktop/latest.json"
|
||||
],
|
||||
"requireSignedVersion": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tag": "v2.0.0-beta.1",
|
||||
"commit": "a3b0e660cc4de3d25d75e560afd8441d0fa37a00",
|
||||
"sha256": "f66cc1ccde81269200a6ca1a17f6ced6e8ccdf77353c8c8e7f74196762507db3"
|
||||
}
|
||||
+645
-56
@@ -1,79 +1,668 @@
|
||||
/* The first-run page. Tokens in tokens.css. */
|
||||
/* LLeMbas Desktop's page. Every colour, size, space and font is a token from tokens.css (the
|
||||
server's, copied by scripts/fetch-tokens.sh and pinned); the only values of this page's own are
|
||||
the title bar's height and the setup card's width, named below. Media queries cannot read a
|
||||
custom property, so the two breakpoints are written out and named after the tokens they are:
|
||||
48rem is --bp-narrow (the sidebar becomes a drawer), 64rem is --bp-wide (the viewer goes over
|
||||
the chat instead of beside it).
|
||||
|
||||
Rows line up by construction: every row of controls sits on a grid with known tracks, a field
|
||||
is always three parts (label, control, hint) on shared rows, and nothing is aligned by
|
||||
`baseline` across text of unknown length. */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
padding: var(--sp-4);
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: var(--text-base) / 1.6 var(--font-body);
|
||||
:root {
|
||||
/* The title bar's height: a fact of this page, not a token the server has. */
|
||||
--titlebar-h: 36px;
|
||||
/* The setup card's widest. */
|
||||
--card-max: 40rem;
|
||||
/* The sidebar toggle's track in the title bar: none until the sidebar is a drawer. */
|
||||
--side-btn-w: 0px;
|
||||
}
|
||||
|
||||
html, body { margin: 0; height: 100%; overflow: hidden; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: var(--text-base) / var(--leading-normal) var(--font-body);
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
.app { display: grid; grid-template-rows: var(--titlebar-h) minmax(0, 1fr); height: 100vh; }
|
||||
|
||||
h1, h2, h3, p { margin: 0; }
|
||||
.muted { color: var(--ink-muted); font-size: var(--text-sm); }
|
||||
.is-error { color: var(--warning); }
|
||||
bdi { unicode-bidi: isolate; }
|
||||
|
||||
.icon { width: 16px; height: 16px; fill: currentColor; flex: none; }
|
||||
|
||||
/* --- Controls ------------------------------------------------------------------------------ */
|
||||
|
||||
.btn {
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--sp-4);
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: 600 var(--text-sm) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
border: var(--border-w) solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-block);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover { background: var(--surface-hover); }
|
||||
.btn:active { transform: translate(var(--press), var(--press)); box-shadow: none; }
|
||||
.btn:disabled { opacity: var(--opacity-dim); cursor: default; }
|
||||
.btn--primary { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); }
|
||||
.btn--primary:hover { background: var(--accent-hover); }
|
||||
.btn--sm { height: var(--control-h-sm); padding: 0 var(--sp-3); box-shadow: none; }
|
||||
|
||||
input[type="text"], select, textarea {
|
||||
min-width: 0;
|
||||
font: var(--text-sm) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: var(--border-w) solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
input[type="text"], select { height: var(--control-h); padding: 0 var(--control-px-sm); }
|
||||
textarea { padding: var(--sp-2) var(--control-px-sm); resize: none; line-height: var(--leading-normal); }
|
||||
:focus-visible { outline: var(--outline-w) solid var(--accent); outline-offset: var(--outline-offset); }
|
||||
|
||||
label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); color: var(--ink-muted); }
|
||||
.hint { font-size: var(--text-sm); color: var(--ink-muted); min-height: calc(var(--text-sm) * var(--leading-normal)); }
|
||||
.status { font: var(--text-sm) / var(--leading-normal) var(--font-ui); }
|
||||
.status.is-ok { color: var(--leaf); }
|
||||
.check { display: grid; grid-template-columns: var(--check-size) minmax(0, 1fr); gap: var(--sp-2); align-items: center; color: var(--ink); }
|
||||
.check input { width: var(--check-size); height: var(--check-size); margin: 0; accent-color: var(--accent); }
|
||||
|
||||
.chip {
|
||||
display: inline-block;
|
||||
padding: 0 var(--sp-2);
|
||||
font: var(--text-xs) / 1.6 var(--font-ui);
|
||||
color: var(--ink-muted);
|
||||
background: var(--surface);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip--leaf { color: var(--leaf); border-color: var(--leaf); }
|
||||
|
||||
.dot { width: var(--dot-size); height: var(--dot-size); background: var(--ink-faint); flex: none; }
|
||||
.dot.is-ok { background: var(--leaf); }
|
||||
.dot.is-busy { background: var(--accent); }
|
||||
.dot.is-new { background: var(--leaf); }
|
||||
.dot.is-error { background: var(--warning); }
|
||||
|
||||
/* A field is three stacked parts on fixed rows: label, control, hint -- always all three. */
|
||||
.field { display: grid; grid-template-rows: auto var(--control-h) auto; gap: var(--sp-2); align-content: start; }
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
grid-auto-rows: auto var(--control-h) auto;
|
||||
column-gap: var(--sp-4);
|
||||
row-gap: var(--sp-2);
|
||||
}
|
||||
/* Each field spans three shared rows, so side-by-side controls sit on one line whatever their
|
||||
labels and hints do. */
|
||||
.fields .field { grid-row: span 3; grid-template-rows: subgrid; }
|
||||
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-2); }
|
||||
.row--buttons { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr)); gap: var(--sp-2); }
|
||||
|
||||
/* A section's heading in brackets, as on the server's screens: `[ SERVER ]`. */
|
||||
.card h2, .set h2 {
|
||||
margin: 0 0 var(--sp-3);
|
||||
font: 600 var(--text-sm) / var(--leading-tight) var(--font-ui);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
.card h2::before, .set h2::before { content: "[ "; }
|
||||
.card h2::after, .set h2::after { content: " ]"; }
|
||||
|
||||
.code {
|
||||
font: 700 var(--text-xl) / var(--leading-tight) var(--font-mono);
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--leaf);
|
||||
margin: var(--sp-2) 0 var(--sp-3);
|
||||
}
|
||||
|
||||
/* --- The title bar ------------------------------------------------------------------------- */
|
||||
|
||||
.bar {
|
||||
display: grid;
|
||||
/* side · leaf · name · host (the rest, and the drag area) · update · agent · menu · sep · 3 controls */
|
||||
grid-template-columns: var(--side-btn-w) var(--titlebar-h) auto minmax(0, 1fr) auto auto var(--titlebar-h) var(--border-w) repeat(3, 2.875rem);
|
||||
align-items: center;
|
||||
height: var(--titlebar-h);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
background: var(--bg);
|
||||
font: 600 var(--text-sm) / 1 var(--font-ui);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
.bar button {
|
||||
height: var(--titlebar-h);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: var(--ink-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bar button:hover { color: var(--ink); background: var(--surface-hover); }
|
||||
.bar button:focus-visible { outline-offset: calc(-1 * var(--outline-w)); }
|
||||
.bar__leaf { justify-self: center; width: 18px; height: 18px; }
|
||||
.bar__name b { color: var(--leaf); font-weight: 700; }
|
||||
.bar__host {
|
||||
padding: 0 var(--sp-3);
|
||||
color: var(--ink-muted);
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.bar__btn { display: grid; place-items: center; }
|
||||
.bar__side { overflow: hidden; visibility: hidden; }
|
||||
.bar__agent { display: grid; grid-template-columns: var(--dot-size) auto; gap: var(--sp-2); align-items: center; padding: 0 var(--sp-3) !important; font-weight: 400 !important; }
|
||||
.bar__agent[data-state="running"] .dot, .bar__agent[data-state="elsewhere"] .dot { background: var(--leaf); }
|
||||
.bar__agent[data-state="starting"] .dot { background: var(--accent); }
|
||||
.bar__agent[data-state="failed"] .dot, .bar__agent[data-state="not_linked"] .dot, .bar__agent[data-state="no_folders"] .dot { background: var(--warning); }
|
||||
.bar__update { padding: 0 var(--sp-3) !important; color: var(--accent-ink) !important; background: var(--accent) !important; }
|
||||
.bar__update:hover { background: var(--accent-hover) !important; }
|
||||
/* Hidden keeps its grid track (auto, so zero wide): the slots after it never move. */
|
||||
.bar__update[hidden] { display: block !important; width: 0; padding: 0 !important; overflow: hidden; visibility: hidden; }
|
||||
.bar__sep { width: var(--border-w); height: var(--sp-5); background: var(--border); }
|
||||
.bar__btn--close:hover { color: var(--ink-inverse) !important; background: var(--danger) !important; }
|
||||
|
||||
/* --- Setup --------------------------------------------------------------------------------- */
|
||||
|
||||
.setup { overflow: auto; display: grid; place-items: center; padding: var(--sp-4); }
|
||||
.card {
|
||||
width: min(100%, var(--card-max));
|
||||
display: grid;
|
||||
gap: var(--sp-6);
|
||||
padding: var(--sp-8);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
box-shadow: var(--shadow-block-lg);
|
||||
}
|
||||
.setup__body { display: grid; gap: var(--sp-6); }
|
||||
.setup__body section { display: grid; gap: var(--sp-3); }
|
||||
.brand { display: grid; grid-template-columns: var(--sp-10) minmax(0, 1fr); gap: var(--sp-3); align-items: center; }
|
||||
.brand img { width: var(--sp-10); height: var(--sp-10); }
|
||||
.brand h1 { font: 600 var(--text-xl) / var(--leading-tight) var(--font-ui); }
|
||||
.brand h1 b { color: var(--leaf); font-weight: 700; }
|
||||
|
||||
/* --- The shell ----------------------------------------------------------------------------- */
|
||||
|
||||
.shell {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
|
||||
grid-template-areas: "side main";
|
||||
}
|
||||
.shell[data-viewer="true"] {
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr) minmax(var(--inspector-width-min), 42%);
|
||||
grid-template-areas: "side main viewer";
|
||||
}
|
||||
|
||||
.brand { display: grid; grid-template-columns: 40px 1fr; gap: var(--sp-3); align-items: center; }
|
||||
h1 { margin: 0; font: 600 var(--text-xl) / 1.2 var(--font-ui); }
|
||||
h1 b { color: var(--leaf); font-weight: 700; }
|
||||
h2 { margin: 0 0 var(--sp-3); font: 600 var(--text-sm) / 1.2 var(--font-ui); letter-spacing: 0.08em; color: var(--accent); }
|
||||
p { margin: 0; }
|
||||
.muted { color: var(--ink-muted); font-size: var(--text-sm); }
|
||||
|
||||
/* A field is three stacked parts on fixed rows: label, control row, hint. */
|
||||
.field { display: grid; grid-template-rows: auto var(--control-h) auto; gap: var(--sp-2); }
|
||||
label { font: var(--text-sm) / 1.4 var(--font-ui); color: var(--ink-muted); }
|
||||
.hint { font-size: var(--text-sm); color: var(--ink-muted); min-height: 1.4em; }
|
||||
.hint.is-error, .status.is-error { color: var(--warning); }
|
||||
|
||||
.row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); }
|
||||
.row--buttons { grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); margin-top: var(--sp-3); }
|
||||
|
||||
input {
|
||||
height: var(--control-h);
|
||||
min-width: 0;
|
||||
padding: 0 var(--sp-3);
|
||||
font: var(--text-base) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
/* The sidebar: the switch, the new-chat button, the chats, then the two explorers (each a row of
|
||||
its own that takes room only while open), then settings. */
|
||||
.side {
|
||||
grid-area: side;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
--local-row: auto;
|
||||
--server-row: auto;
|
||||
grid-template-rows: auto auto minmax(calc(3 * var(--row-h)), 2fr) var(--local-row) var(--server-row) auto;
|
||||
border-right: var(--border-w) solid var(--border);
|
||||
background: var(--bg-sunken);
|
||||
}
|
||||
input:focus-visible, .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.side[data-local="true"] { --local-row: minmax(calc(4 * var(--row-h)), 1.5fr); }
|
||||
.side[data-server="true"] { --server-row: minmax(calc(4 * var(--row-h)), 1.5fr); }
|
||||
|
||||
.btn {
|
||||
.switch { display: grid; grid-template-columns: 1fr 1fr; border-bottom: var(--border-w) solid var(--border); }
|
||||
.switch button {
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--sp-4);
|
||||
font: 600 var(--text-sm) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--press) var(--press) 0 rgba(0, 0, 0, 0.45);
|
||||
color: var(--ink-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: var(--border-w-accent) solid transparent;
|
||||
border-radius: var(--radius-none);
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover { background: var(--surface-hover); }
|
||||
.btn:active { transform: translate(var(--press), var(--press)); box-shadow: none; }
|
||||
.btn:disabled { opacity: 0.55; cursor: default; }
|
||||
.btn--primary { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); }
|
||||
.btn--primary:hover { background: var(--accent-hover); }
|
||||
.switch button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); background: var(--surface); }
|
||||
.side__new { margin: var(--sp-3); }
|
||||
|
||||
.status { font: var(--text-sm) / 1.4 var(--font-ui); margin-top: var(--sp-3); }
|
||||
.status.is-ok { color: var(--leaf); }
|
||||
.list { list-style: none; margin: 0; padding: 0 0 var(--sp-2); overflow: auto; min-height: 0; }
|
||||
.list__head { padding: var(--sp-2) var(--sp-3) var(--sp-1); font: 600 var(--text-xs) var(--font-ui); text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-faint); }
|
||||
.list__note { padding: var(--sp-3); font-size: var(--text-sm); color: var(--ink-muted); }
|
||||
.list__more { margin: var(--sp-2) var(--sp-3); }
|
||||
/* A chat's row: dot · title · when, and under them its folder -- fixed tracks, so rows with long
|
||||
titles or no folder keep their dots and times on the same lines. */
|
||||
.chatrow {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: var(--dot-size) minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--row-h);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
text-align: left;
|
||||
font: var(--text-sm) / var(--leading-tight) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: var(--border-w-accent) solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chatrow:hover { background: var(--surface-hover); }
|
||||
.chatrow[aria-current="true"] { background: var(--surface-active); border-left-color: var(--accent); }
|
||||
.chatrow.is-unread .row__title { font-weight: 700; }
|
||||
.row__title, .row__sub { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.row__when { font-size: var(--text-xs); color: var(--ink-faint); }
|
||||
.row__sub { grid-column: 2 / 4; font-size: var(--text-xs); color: var(--ink-muted); direction: rtl; text-align: left; }
|
||||
|
||||
/* The explorers. */
|
||||
.pane { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); border-top: var(--border-w) solid var(--border); }
|
||||
.pane[data-open="false"] .pane__body { display: none; }
|
||||
.pane__head { display: grid; grid-template-columns: minmax(0, 1fr) var(--control-h-sm); align-items: center; }
|
||||
.pane__toggle {
|
||||
height: var(--control-h);
|
||||
display: grid;
|
||||
grid-template-columns: 1rem minmax(0, 1fr);
|
||||
gap: var(--sp-1);
|
||||
align-items: center;
|
||||
padding: 0 var(--sp-3);
|
||||
text-align: left;
|
||||
font: 600 var(--text-xs) var(--font-ui);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--ink-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pane__toggle[aria-expanded="true"] .tree__chevron .icon { transform: rotate(90deg); }
|
||||
.pane__btn {
|
||||
width: var(--control-h-sm);
|
||||
height: var(--control-h-sm);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
color: var(--ink-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
.pane__btn:hover { color: var(--ink); background: var(--surface-hover); }
|
||||
.pane__body { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
||||
.pane__where { padding: 0 var(--sp-3) var(--sp-2); }
|
||||
.pane__where:empty { display: none; }
|
||||
.pane__pick { width: 100%; height: var(--control-h-sm); }
|
||||
.pane__path { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-xs) var(--font-ui); color: var(--ink-muted); }
|
||||
.pane__tree { overflow: auto; min-height: 0; padding-bottom: var(--sp-2); }
|
||||
.pane__note { padding: var(--sp-2) var(--sp-3); font-size: var(--text-sm); color: var(--ink-muted); }
|
||||
.pane__tree > .btn { margin: 0 var(--sp-3); }
|
||||
|
||||
.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
|
||||
.tree__row {
|
||||
width: 100%;
|
||||
height: var(--row-h);
|
||||
display: grid;
|
||||
grid-template-columns: calc(var(--depth, 0) * var(--sp-3)) 1rem 1rem minmax(0, 1fr) auto;
|
||||
gap: var(--sp-1);
|
||||
align-items: center;
|
||||
padding: 0 var(--sp-3);
|
||||
text-align: left;
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* The indent's track needs an item of its own, or the chevron would sit in it. */
|
||||
.tree__row::before { content: ""; }
|
||||
.tree__row:hover { background: var(--surface-hover); }
|
||||
.tree__row .icon { color: var(--ink-muted); }
|
||||
.tree__chevron { display: grid; place-items: center; width: 1rem; }
|
||||
.tree__chevron .icon { width: 12px; height: 12px; transition: transform var(--dur-1) var(--ease-out); }
|
||||
.tree__row[aria-expanded="true"] .tree__chevron .icon { transform: rotate(90deg); }
|
||||
.tree__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.tree__size { color: var(--ink-faint); }
|
||||
.tree__note { padding: var(--sp-1) var(--sp-3); font-size: var(--text-xs); color: var(--ink-muted); }
|
||||
|
||||
.side__settings {
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr);
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--sp-3);
|
||||
text-align: left;
|
||||
font: var(--text-sm) var(--font-ui);
|
||||
color: var(--ink-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-top: var(--border-w) solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.side__settings:hover { color: var(--ink); background: var(--surface-hover); }
|
||||
|
||||
.scrim { display: none; }
|
||||
|
||||
/* --- The chat ------------------------------------------------------------------------------ */
|
||||
|
||||
.main, .settings { grid-area: main; min-width: 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; }
|
||||
.settings { grid-template-rows: auto minmax(0, 1fr); }
|
||||
|
||||
.head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto;
|
||||
gap: var(--sp-3);
|
||||
align-items: center;
|
||||
min-height: var(--header-height);
|
||||
padding: 0 var(--sp-4);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
}
|
||||
.head__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-ui); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.head__place { display: flex; gap: var(--sp-2); align-items: center; min-width: 0; }
|
||||
.head__dir { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-xs) var(--font-ui); color: var(--ink-muted); min-width: 0; }
|
||||
.head__tools { display: flex; gap: var(--sp-2); align-items: center; }
|
||||
.head__mode { height: var(--control-h-sm); }
|
||||
|
||||
.thread { overflow: auto; min-height: 0; padding: var(--sp-4); scroll-behavior: auto; }
|
||||
.thread > * { max-width: var(--thread-max-width); margin-left: auto; margin-right: auto; }
|
||||
.thread__note { padding: var(--sp-2) 0; font-size: var(--text-sm); }
|
||||
.thread__loading { padding: var(--sp-4) 0; }
|
||||
.thread__earlier { display: block; margin: 0 auto var(--sp-4); }
|
||||
.messages { display: grid; gap: var(--sp-5); }
|
||||
|
||||
.new { display: grid; gap: var(--sp-4); padding: var(--sp-4) 0; }
|
||||
.new__warn { color: var(--warning); font-size: var(--text-sm); }
|
||||
.new select { width: 100%; }
|
||||
|
||||
.msg { display: grid; gap: var(--sp-3); min-width: 0; }
|
||||
.msg--user {
|
||||
justify-self: end;
|
||||
max-width: min(100%, var(--w-prose));
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
background: var(--bubble-user);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.msg--user.is-queued { opacity: var(--opacity-dim); }
|
||||
.msg--user.is-machine { justify-self: stretch; max-width: none; font-size: var(--text-sm); color: var(--ink-muted); background: transparent; }
|
||||
.msg__text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.msg__files { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-1); font-size: var(--text-xs); }
|
||||
.msg__note { font-size: var(--text-sm); color: var(--ink-muted); }
|
||||
.msg__foot { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
|
||||
.msg__metrics { font: var(--text-xs) var(--font-ui); color: var(--ink-faint); }
|
||||
.msg__steps { display: grid; gap: var(--sp-3); min-width: 0; }
|
||||
.msg__status, .msg__thinking { font: var(--text-xs) var(--font-ui); }
|
||||
.msg__status.is-error { color: var(--warning); }
|
||||
.msg__jobs { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--sp-1); font-size: var(--text-sm); }
|
||||
.job { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-2); align-items: center; }
|
||||
|
||||
.step--thinking summary, .call__head { cursor: pointer; font: var(--text-sm) var(--font-ui); color: var(--ink-muted); }
|
||||
.step__pre { margin-top: var(--sp-2); padding-left: var(--sp-3); border-left: var(--border-w-thick) solid var(--border); white-space: pre-wrap; font-size: var(--text-sm); color: var(--ink-muted); }
|
||||
.step--tools { display: grid; gap: var(--sp-1); }
|
||||
.step--steer { padding-left: var(--sp-3); border-left: var(--border-w-thick) solid var(--accent-soft); }
|
||||
.steer__text { white-space: pre-wrap; font-size: var(--text-sm); }
|
||||
|
||||
.call { border: var(--border-w) solid var(--border); border-radius: var(--radius); background: var(--surface); min-width: 0; }
|
||||
.call.is-failed { border-color: var(--warning); }
|
||||
/* The call's line: dot · title · chips, on fixed tracks. */
|
||||
.call__head {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: var(--dot-size) minmax(0, 1fr) auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--row-h);
|
||||
padding: 0 var(--sp-3);
|
||||
}
|
||||
.call__head::-webkit-details-marker { display: none; }
|
||||
.call__title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--ink); font-family: var(--font-mono); }
|
||||
.call__chips { display: flex; gap: var(--sp-1); }
|
||||
.call__body { display: grid; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3) var(--sp-3); border-top: var(--border-w) solid var(--border); }
|
||||
.call__pre, .md-code pre {
|
||||
margin: 0;
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
max-height: var(--h-list);
|
||||
overflow: auto;
|
||||
font: var(--text-xs) / var(--leading-normal) var(--font-mono);
|
||||
background: var(--code-bg);
|
||||
border: var(--border-w) solid var(--code-border);
|
||||
border-radius: var(--radius);
|
||||
white-space: pre;
|
||||
}
|
||||
.call__open { justify-self: start; margin: 0 var(--sp-3) var(--sp-3); }
|
||||
|
||||
/* Markdown, drawn by markdown.js from model output (never as HTML). */
|
||||
.md { overflow-wrap: anywhere; min-width: 0; }
|
||||
.md > * + * { margin-top: var(--sp-3); }
|
||||
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 { font-family: var(--font-ui); line-height: var(--leading-tight); }
|
||||
.md h1 { font-size: var(--text-xl); }
|
||||
.md h2 { font-size: var(--text-lg); }
|
||||
.md h3 { font-size: var(--text-md); }
|
||||
.md h4, .md h5, .md h6 { font-size: var(--text-base); }
|
||||
.md ul, .md ol { margin: 0; padding-left: var(--sp-6); }
|
||||
.md li + li { margin-top: var(--sp-1); }
|
||||
.md-task { list-style: none; margin-left: calc(-1 * var(--sp-5)); }
|
||||
.md-task input { margin: 0 var(--sp-2) 0 0; }
|
||||
.md blockquote { margin: 0; padding-left: var(--sp-3); border-left: var(--border-w-thick) solid var(--border-strong); color: var(--ink-muted); }
|
||||
.md hr { border: 0; border-top: var(--border-w) solid var(--border); }
|
||||
.md code { font: 0.9em var(--font-mono); padding: 0 var(--sp-1); background: var(--code-bg); border-radius: var(--radius-sm); }
|
||||
.md pre code { padding: 0; background: none; }
|
||||
.md a { color: var(--accent); }
|
||||
.md a.md-unsafe { color: var(--ink-muted); text-decoration: line-through dotted; }
|
||||
.md-code { position: relative; display: grid; }
|
||||
.md-lang { position: absolute; top: var(--sp-1); left: var(--sp-3); font: var(--text-xs) var(--font-ui); color: var(--ink-faint); }
|
||||
.md-code:has(.md-lang) pre { padding-top: var(--sp-6); }
|
||||
.md-copy {
|
||||
position: absolute;
|
||||
top: var(--sp-1);
|
||||
right: var(--sp-1);
|
||||
height: var(--control-h-sm);
|
||||
padding: 0 var(--sp-2);
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
color: var(--ink-muted);
|
||||
background: var(--surface);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
/* A wide table scrolls by itself; the page does not. */
|
||||
.md-table { overflow-x: auto; }
|
||||
.md table { border-collapse: collapse; font-size: var(--text-sm); }
|
||||
.md th, .md td { padding: var(--sp-1) var(--sp-3); border: var(--border-w) solid var(--border); text-align: left; vertical-align: top; }
|
||||
.md [data-align="center"] { text-align: center; }
|
||||
.md [data-align="right"] { text-align: right; }
|
||||
.md th { background: var(--surface); }
|
||||
|
||||
/* Cards: what waits for an answer, above the composer. */
|
||||
.cards { overflow: auto; max-height: 50vh; padding: var(--sp-3) var(--sp-4) 0; display: grid; gap: var(--sp-3); }
|
||||
.ask {
|
||||
width: 100%;
|
||||
max-width: var(--thread-max-width);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
background: var(--surface-raised);
|
||||
border: var(--border-w-accent) solid var(--accent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-block-accent);
|
||||
}
|
||||
.ask__item { display: grid; gap: var(--sp-2); }
|
||||
.ask__header { font: 600 var(--text-xs) var(--font-ui); text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); }
|
||||
.ask__title { font-weight: 600; }
|
||||
.ask__edit { width: 100%; font-family: var(--font-mono); }
|
||||
.ask__text { width: 100%; }
|
||||
.ask__options { display: grid; gap: var(--sp-2); }
|
||||
.opt {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--control-h);
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
text-align: left;
|
||||
font: var(--text-sm) var(--font-body);
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: var(--border-w) solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
.opt[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.opt__desc { grid-column: 1; font-size: var(--text-xs); color: var(--ink-muted); }
|
||||
.opt .chip { grid-column: 2; grid-row: 1; }
|
||||
.ask__actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr)); gap: var(--sp-2); }
|
||||
.ask__said { font-size: var(--text-sm); color: var(--warning); }
|
||||
.ask__said:empty { display: none; }
|
||||
.plan { margin: 0; padding-left: var(--sp-5); display: grid; gap: var(--sp-2); font-size: var(--text-sm); }
|
||||
.plan ul { margin: var(--sp-1) 0 0; padding-left: var(--sp-4); }
|
||||
.plan__phase { font-weight: 600; }
|
||||
.plan__task.is-done { text-decoration: line-through; color: var(--ink-muted); }
|
||||
|
||||
/* The composer: the text and its buttons, on two tracks; the buttons hold their own row. */
|
||||
.composer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: end;
|
||||
padding: var(--sp-3) var(--sp-4) var(--sp-4);
|
||||
}
|
||||
.composer > * { min-width: 0; }
|
||||
.composer textarea { width: 100%; min-height: var(--control-h); max-height: 40vh; font: var(--text-base) / var(--leading-normal) var(--font-body); }
|
||||
.composer__actions { display: grid; grid-auto-flow: column; gap: var(--sp-2); }
|
||||
|
||||
/* --- Settings ------------------------------------------------------------------------------ */
|
||||
|
||||
.settings__body { overflow: auto; padding: var(--sp-4); display: grid; gap: var(--sp-6); align-content: start; }
|
||||
.settings__body > .set { width: 100%; max-width: var(--w-prose); margin: 0 auto; display: grid; gap: var(--sp-3); }
|
||||
/* The folder list: every row the same two tracks, the path cut from the left (its end tells
|
||||
folders apart), the full path in the title. */
|
||||
.folders { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
|
||||
.folders li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0 0 var(--control-px);
|
||||
background: var(--bg);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.folders .path { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-sm) var(--font-ui); }
|
||||
.folders .btn { height: calc(var(--control-h) - 2 * var(--border-w)); box-shadow: none; border-width: 0 0 0 var(--border-w); }
|
||||
.folders .empty { padding: var(--control-px); color: var(--warning); font-size: var(--text-sm); }
|
||||
|
||||
/* --- The viewer ---------------------------------------------------------------------------- */
|
||||
|
||||
.viewer {
|
||||
grid-area: viewer;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
border-left: var(--border-w) solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
.viewer__head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto var(--control-h-sm);
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--header-height);
|
||||
padding: 0 var(--sp-3);
|
||||
border-bottom: var(--border-w) solid var(--border);
|
||||
}
|
||||
.viewer__id { min-width: 0; }
|
||||
.viewer__name { font: 600 var(--text-sm) / var(--leading-tight) var(--font-ui); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.viewer__where { font: var(--text-xs) var(--font-ui); color: var(--ink-muted); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.viewer__modes { display: grid; grid-auto-flow: column; }
|
||||
.seg {
|
||||
height: var(--control-h-sm);
|
||||
padding: 0 var(--sp-3);
|
||||
font: var(--text-xs) var(--font-ui);
|
||||
color: var(--ink-muted);
|
||||
background: var(--surface);
|
||||
border: var(--border-w) solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.seg + .seg { border-left: 0; }
|
||||
.seg[aria-pressed="true"] { color: var(--ink); background: var(--surface-active); }
|
||||
.viewer__body { min-height: 0; display: grid; }
|
||||
.viewer__code { min-height: 0; overflow: hidden; }
|
||||
.viewer__code .cm-editor { height: 100%; }
|
||||
.viewer__md { overflow: auto; padding: var(--sp-4); }
|
||||
.viewer__frame { width: 100%; height: 100%; border: 0; background: var(--surface); }
|
||||
.viewer__note { padding: var(--sp-4); font-size: var(--text-sm); color: var(--ink-muted); }
|
||||
.viewer__note.is-error { color: var(--warning); }
|
||||
|
||||
/* --- Narrower windows ---------------------------------------------------------------------- */
|
||||
|
||||
/* 64rem (--bp-wide): the viewer goes over the chat instead of beside it. */
|
||||
@media (max-width: 64rem) {
|
||||
.shell[data-viewer="true"] { grid-template-columns: var(--sidebar-width) minmax(0, 1fr); grid-template-areas: "side main"; }
|
||||
.viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; }
|
||||
}
|
||||
|
||||
/* 48rem (--bp-narrow): the sidebar is a drawer, opened from the title bar. */
|
||||
@media (max-width: 48rem) {
|
||||
:root { --side-btn-w: var(--titlebar-h); }
|
||||
.bar__side { visibility: visible; }
|
||||
.bar__host { display: none; }
|
||||
.shell, .shell[data-viewer="true"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "main"; }
|
||||
.side {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: min(85vw, var(--sidebar-width));
|
||||
z-index: var(--z-panel);
|
||||
transform: translateX(-100%);
|
||||
transition: transform var(--dur-2) var(--ease-out);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.shell[data-side="true"] .side { transform: none; }
|
||||
.shell[data-side="true"] .scrim { display: block; position: absolute; inset: 0; z-index: calc(var(--z-panel) - 1); background: var(--scrim); }
|
||||
.viewer { z-index: var(--z-panel); }
|
||||
.head { padding: 0 var(--sp-3); grid-template-columns: minmax(0, 1fr) auto; }
|
||||
.head__place { grid-column: 1 / -1; grid-row: 2; padding-bottom: var(--sp-2); }
|
||||
.thread { padding: var(--sp-3); }
|
||||
.composer { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
|
||||
.cards { padding: var(--sp-2) var(--sp-3) 0; }
|
||||
.card { padding: var(--sp-4); gap: var(--sp-4); }
|
||||
.msg--user { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* Very narrow: the agent's words go, its dot stays; the row's button goes under its field. */
|
||||
@media (max-width: 30rem) {
|
||||
.card { padding: var(--sp-4); gap: var(--sp-4); }
|
||||
.row { grid-template-columns: 1fr; grid-template-rows: var(--control-h) var(--control-h); }
|
||||
.field { grid-template-rows: auto auto auto; }
|
||||
#agent-text { display: none; }
|
||||
.setup .row { grid-template-columns: minmax(0, 1fr); }
|
||||
.setup .field { grid-template-rows: auto auto auto; }
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// The first-run page. It talks to the app over Tauri's IPC — four commands, see src-tauri/src/lib.rs.
|
||||
// The server's own pages never get this: they are remote content with no capability.
|
||||
"use strict"
|
||||
|
||||
const { invoke } = window.__TAURI__.core
|
||||
const { listen } = window.__TAURI__.event
|
||||
|
||||
const $ = (id) => document.getElementById(id)
|
||||
|
||||
const AGENT = {
|
||||
off: () => ["The agent is off.", ""],
|
||||
not_linked: () => ["This computer is not linked to the server yet.", ""],
|
||||
starting: () => ["The agent is starting…", ""],
|
||||
running: () => ["The agent is running: agent chats can use this computer.", "is-ok"],
|
||||
elsewhere: (pid) => [`A LLeMbas CLI service already runs here (pid ${pid}); it is this computer's agent.`, "is-ok"],
|
||||
failed: (why) => [`The agent stopped: ${why}. Trying again shortly.`, "is-error"],
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
$("version").textContent = s.version
|
||||
if (s.server && !$("address").value) $("address").value = s.server
|
||||
$("agent-section").hidden = !s.server
|
||||
const [text, cls] = (AGENT[s.agent.state] ?? AGENT.off)(s.agent.detail)
|
||||
const status = $("agent-status")
|
||||
status.textContent = text
|
||||
status.className = `status ${cls}`
|
||||
$("link").textContent = s.linked ? "Link again" : "Link this computer"
|
||||
$("toggle").textContent = s.agent_wanted ? "Stop the agent" : "Start the agent"
|
||||
$("toggle").disabled = !s.linked && !s.agent_wanted
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
render(await invoke("get_status"))
|
||||
}
|
||||
|
||||
$("server-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault()
|
||||
const hint = $("server-hint")
|
||||
const button = e.target.querySelector("button")
|
||||
button.disabled = true
|
||||
hint.className = "hint"
|
||||
hint.textContent = "Looking for a LLeMbas server there…"
|
||||
try {
|
||||
await invoke("set_server", { address: $("address").value })
|
||||
// The window now shows the server.
|
||||
} catch (err) {
|
||||
hint.className = "hint is-error"
|
||||
hint.textContent = String(err)
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
})
|
||||
|
||||
$("link").addEventListener("click", async () => {
|
||||
try {
|
||||
await invoke("link_computer")
|
||||
} catch (err) {
|
||||
$("agent-status").className = "status is-error"
|
||||
$("agent-status").textContent = String(err)
|
||||
}
|
||||
})
|
||||
|
||||
$("toggle").addEventListener("click", async () => {
|
||||
const s = await invoke("get_status")
|
||||
await invoke("set_agent", { on: !s.agent_wanted })
|
||||
})
|
||||
|
||||
$("open").addEventListener("click", async () => {
|
||||
const s = await invoke("get_status")
|
||||
if (s.server) await invoke("set_server", { address: s.server })
|
||||
})
|
||||
|
||||
listen("status", (e) => render(e.payload))
|
||||
refresh()
|
||||
+1047
File diff suppressed because it is too large.
Load diff
+107
-33
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="moria" data-base="moria">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -7,42 +7,116 @@
|
||||
<title>LLeMbas</title>
|
||||
<link rel="stylesheet" href="tokens.css">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<script src="app.js" defer></script>
|
||||
<script src="i18n.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card" aria-labelledby="title">
|
||||
<header class="brand">
|
||||
<img src="logo-mark.svg" alt="" width="40" height="40">
|
||||
<div>
|
||||
<h1 id="title"><b>LL</b>e<b>M</b>bas</h1>
|
||||
<p class="muted">Desktop <span id="version"></span></p>
|
||||
</div>
|
||||
<div class="app">
|
||||
<header class="bar" data-tauri-drag-region>
|
||||
<button class="bar__btn bar__side" id="side-toggle" type="button" aria-label="Chats" title="Chats" aria-expanded="false" aria-controls="side" data-i18n-aria-label data-i18n-title></button>
|
||||
<img class="bar__leaf" src="logo-mark.svg" alt="" width="18" height="18" data-tauri-drag-region>
|
||||
<span class="bar__name" data-tauri-drag-region><b>LL</b>e<b>M</b>bas</span>
|
||||
<span class="bar__host" id="host" data-tauri-drag-region></span>
|
||||
<button class="bar__update" id="update" type="button" hidden>update</button>
|
||||
<button class="bar__agent" id="agent" type="button" title="Agent">
|
||||
<span class="dot" aria-hidden="true"></span><span id="agent-text">agent off</span>
|
||||
</button>
|
||||
<button class="bar__btn" id="menu" type="button" aria-label="Menu" title="Menu" data-i18n-aria-label data-i18n-title></button>
|
||||
<span class="bar__sep" aria-hidden="true"></span>
|
||||
<button class="bar__btn" id="minimize" type="button" aria-label="Minimise" title="Minimise" data-i18n-aria-label data-i18n-title></button>
|
||||
<button class="bar__btn" id="maximize" type="button" aria-label="Maximise" title="Maximise" data-i18n-aria-label data-i18n-title></button>
|
||||
<button class="bar__btn bar__btn--close" id="close" type="button" aria-label="Close" title="Close (LLeMbas stays in the tray)" data-i18n-aria-label data-i18n-title></button>
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="server-h">
|
||||
<h2 id="server-h">[ SERVER ]</h2>
|
||||
<form id="server-form" class="field" novalidate>
|
||||
<label for="address">Address of your LLeMbas server</label>
|
||||
<div class="row">
|
||||
<input id="address" name="address" type="text" inputmode="url" autocomplete="url"
|
||||
placeholder="https://ai.example" spellcheck="false" required>
|
||||
<button type="submit" class="btn btn--primary">Connect</button>
|
||||
</div>
|
||||
<p class="hint" id="server-hint" aria-live="polite">Sign in there as you would in a browser.</p>
|
||||
</form>
|
||||
</section>
|
||||
<div class="setup" id="setup" hidden>
|
||||
<main class="card" aria-labelledby="setup-title">
|
||||
<header class="brand">
|
||||
<img src="logo-mark.svg" alt="" width="40" height="40">
|
||||
<div>
|
||||
<h1 id="setup-title"><b>LL</b>e<b>M</b>bas</h1>
|
||||
<p class="muted">Desktop <span id="setup-version"></span></p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="setup__body" id="setup-body"></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="agent-h" id="agent-section" hidden>
|
||||
<h2 id="agent-h">[ AGENT ]</h2>
|
||||
<p class="muted">The agent lets agent chats on your server work in this computer's projects.
|
||||
It is the LLeMbas CLI, built in, running in the background while the app runs.</p>
|
||||
<p class="status" id="agent-status" aria-live="polite"></p>
|
||||
<div class="row row--buttons">
|
||||
<button type="button" class="btn" id="link">Link this computer</button>
|
||||
<button type="button" class="btn" id="toggle">Start the agent</button>
|
||||
<button type="button" class="btn btn--primary" id="open">Open the server</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div class="shell" id="shell" hidden data-side="false" data-viewer="false">
|
||||
<nav class="side" id="side" aria-label="Chats" data-i18n-aria-label data-local="true" data-server="false">
|
||||
<div class="switch" role="tablist">
|
||||
<button type="button" role="tab" data-kind="agent" aria-selected="true" data-i18n>Agent</button>
|
||||
<button type="button" role="tab" data-kind="chat" aria-selected="false" data-i18n>Chats</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn--primary side__new" id="new-chat-btn"><span data-i18n>New agent chat</span></button>
|
||||
<ul class="list" id="chat-list"></ul>
|
||||
|
||||
<section class="pane" id="pane-local" data-open="true" aria-labelledby="pane-local-h">
|
||||
<div class="pane__head">
|
||||
<button type="button" class="pane__toggle" aria-expanded="true" id="pane-local-h"><span data-i18n>This computer</span></button>
|
||||
<button type="button" class="pane__btn" id="local-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button>
|
||||
</div>
|
||||
<div class="pane__body">
|
||||
<div class="pane__where" id="local-where"></div>
|
||||
<div class="pane__tree" id="local-tree"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane" id="pane-server" data-open="false" aria-labelledby="pane-server-h">
|
||||
<div class="pane__head">
|
||||
<button type="button" class="pane__toggle" aria-expanded="false" id="pane-server-h"><span data-i18n>Server files</span></button>
|
||||
<button type="button" class="pane__btn" id="server-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button>
|
||||
</div>
|
||||
<div class="pane__body">
|
||||
<div class="pane__tree" id="server-tree"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" class="side__settings" id="settings-btn"></button>
|
||||
</nav>
|
||||
<div class="scrim" id="scrim" aria-hidden="true"></div>
|
||||
|
||||
<main class="main" id="main" aria-labelledby="chat-title">
|
||||
<header class="head">
|
||||
<h1 class="head__title" id="chat-title"></h1>
|
||||
<div class="head__place" id="chat-place"></div>
|
||||
<div class="head__tools" id="chat-tools"></div>
|
||||
</header>
|
||||
<div class="thread" id="thread">
|
||||
<p class="thread__note is-error" id="thread-note" hidden></p>
|
||||
<div class="new" id="new-chat" hidden></div>
|
||||
<div class="messages" id="messages"></div>
|
||||
</div>
|
||||
<div class="cards" id="cards" hidden></div>
|
||||
<form class="composer" id="composer" hidden data-running="false">
|
||||
<textarea id="composer-text" rows="1" placeholder="Message" aria-label="Message" data-i18n-placeholder data-i18n-aria-label></textarea>
|
||||
<div class="composer__actions">
|
||||
<button type="button" class="btn" id="stop" hidden data-i18n>Stop</button>
|
||||
<button type="submit" class="btn btn--primary" id="send" data-i18n>Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<section class="settings" id="settings" hidden aria-labelledby="settings-title">
|
||||
<header class="head">
|
||||
<h1 class="head__title" id="settings-title" data-i18n>Settings</h1>
|
||||
<div class="head__place"></div>
|
||||
<div class="head__tools"><button type="button" class="pane__btn" id="settings-close" aria-label="Close" title="Close" data-i18n-aria-label data-i18n-title></button></div>
|
||||
</header>
|
||||
<div class="settings__body" id="settings-body"></div>
|
||||
</section>
|
||||
|
||||
<aside class="viewer" id="viewer" hidden aria-labelledby="viewer-name">
|
||||
<header class="viewer__head">
|
||||
<div class="viewer__id">
|
||||
<h2 class="viewer__name" id="viewer-name"></h2>
|
||||
<p class="viewer__where" id="viewer-where"></p>
|
||||
</div>
|
||||
<div class="viewer__modes" id="viewer-modes"></div>
|
||||
<button type="button" class="pane__btn" id="viewer-close" aria-label="Close" title="Close" data-i18n-aria-label data-i18n-title></button>
|
||||
</header>
|
||||
<div class="viewer__body" id="viewer-body"></div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
// The page's one way out: Tauri's commands (src-tauri/src/lib.rs). The page holds no token and
|
||||
// makes no request of its own -- the content security policy allows it none but the bridge. The
|
||||
// server is reached through `api` and `follow`, which Rust sends with the app's sign-in.
|
||||
|
||||
const tauri = window.__TAURI__
|
||||
export const invoke = tauri.core.invoke
|
||||
export const listen = tauri.event.listen
|
||||
export const win = tauri.window.getCurrentWindow()
|
||||
|
||||
/** A request to the server's /api/v2: `path` from there on. Rejects with {status, detail}. */
|
||||
export function api(method, path, body) {
|
||||
return invoke("api", { method, path, body: body === undefined ? null : body })
|
||||
}
|
||||
export const get = (path) => api("GET", path)
|
||||
export const post = (path, body) => api("POST", path, body ?? {})
|
||||
export const patch = (path, body) => api("PATCH", path, body)
|
||||
export const del = (path) => api("DELETE", path)
|
||||
/** A `GET` on /api/v2 that answers plain text (a shown file). Rejects with {status, detail}. */
|
||||
export const getText = (path) => invoke("api_text", { path })
|
||||
|
||||
/** An error's words, whatever shape it came in. */
|
||||
export function why(err) {
|
||||
if (err && typeof err === "object" && "detail" in err) return String(err.detail)
|
||||
return String(err)
|
||||
}
|
||||
|
||||
// Frames of every followed reply arrive as one event; they are handed on by stream id. A frame
|
||||
// can arrive before `stream_open` has answered with its id, so frames for an unknown id wait.
|
||||
const followers = new Map()
|
||||
const early = new Map()
|
||||
listen("stream", (e) => {
|
||||
const { id, event, data } = e.payload
|
||||
const f = followers.get(id)
|
||||
if (f) deliver(f, id, event, data)
|
||||
else {
|
||||
if (!early.has(id)) early.set(id, [])
|
||||
early.get(id).push([event, data])
|
||||
}
|
||||
})
|
||||
|
||||
function deliver(f, id, event, data) {
|
||||
if (event === "end") {
|
||||
followers.delete(id)
|
||||
f.onEnd(data)
|
||||
} else f.onFrame(event, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow a reply: `onFrame(event, data)` for each frame, `onEnd(error|null)` once. The handle's
|
||||
* `close()` stops it (onEnd still comes).
|
||||
*/
|
||||
export async function follow(path, { onFrame, onEnd }) {
|
||||
const id = await invoke("stream_open", { path })
|
||||
const f = { onFrame, onEnd }
|
||||
followers.set(id, f)
|
||||
for (const [event, data] of early.get(id) || []) {
|
||||
if (!followers.has(id)) break
|
||||
deliver(f, id, event, data)
|
||||
}
|
||||
early.delete(id)
|
||||
return {
|
||||
id,
|
||||
close() {
|
||||
invoke("stream_close", { id })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** A web link, in the system's browser. */
|
||||
export function openLink(url) {
|
||||
return invoke("open_external", { url }).catch(() => {})
|
||||
}
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
// One chat: its transcript, the reply being written (followed as the server streams it), the
|
||||
// cards that wait for an answer, and the composer. And the form a new chat starts from -- an
|
||||
// agent chat on this computer, in one of the agent's folders, or a chat on the server.
|
||||
//
|
||||
// The transcript is the server's /api/v2 serialisation (tests/fixtures/api_v2 in the server's
|
||||
// repository is the contract): a person's message is their text as typed; a reply is its steps --
|
||||
// thinking, text (markdown, model output), tool calls, messages taken in while it ran.
|
||||
|
||||
import { $, h, clear, append } from "./dom.js"
|
||||
import { api, get, getText, post, patch, follow, why, invoke } from "./bridge.js"
|
||||
import { markdown } from "./markdown.js"
|
||||
import { openViewer, showKind } from "./viewer.js"
|
||||
import { readLocal } from "./explorer.js"
|
||||
|
||||
const MODES = ["manual", "edit", "auto", "plan"]
|
||||
|
||||
let ctx = null // set by initChat: { status(), thisDevices(), models(), me(), changed(chat) }
|
||||
let open = null // { chat, live, more, first }
|
||||
|
||||
export function openChatId() {
|
||||
return open?.chat?.id || null
|
||||
}
|
||||
|
||||
// --- Reading -----------------------------------------------------------------------------------
|
||||
|
||||
function onThisComputer(chat) {
|
||||
return !!chat?.device && ctx.thisDevices().includes(chat.device.token_id)
|
||||
}
|
||||
|
||||
function nearBottom() {
|
||||
const th = $("thread")
|
||||
return th.scrollHeight - th.scrollTop - th.clientHeight < 80
|
||||
}
|
||||
|
||||
function toBottom(force) {
|
||||
const th = $("thread")
|
||||
if (force || nearBottom()) th.scrollTop = th.scrollHeight
|
||||
}
|
||||
|
||||
function userMessage(m) {
|
||||
return h(
|
||||
"article",
|
||||
{ class: `msg msg--user${m.queued ? " is-queued" : ""}${m.machine ? " is-machine" : ""}`, dataset: { id: m.id } },
|
||||
h("div", { class: "msg__text", text: m.text || "" }),
|
||||
m.attachments?.length ? h("ul", { class: "msg__files" }, m.attachments.map((a) => h("li", { text: a.name }))) : null,
|
||||
m.queued ? h("p", { class: "msg__note", text: t("Waiting for the reply before it") }) : null,
|
||||
)
|
||||
}
|
||||
|
||||
function metricsLine(x) {
|
||||
if (!x) return null
|
||||
const parts = []
|
||||
if (x.total_tokens) parts.push(t("%(n)s tokens", { n: x.total_tokens.toLocaleString() }))
|
||||
if (x.tok_s) parts.push(`${x.tok_s} tok/s`)
|
||||
if (x.elapsed_ms) parts.push(`${(x.elapsed_ms / 1000).toFixed(1)} s`)
|
||||
if (x.context_limit && x.percent !== undefined) parts.push(`${x.percent} % ctx`)
|
||||
return parts.length ? h("p", { class: "msg__metrics", text: parts.join(" · ") }) : null
|
||||
}
|
||||
|
||||
function assistantMessage(m, chat) {
|
||||
const el = h("article", { class: "msg msg--assistant", dataset: { id: m.id } })
|
||||
const steps = h("div", { class: "msg__steps" })
|
||||
renderSteps(steps, m.steps || [], chat)
|
||||
el.append(steps)
|
||||
if (m.error) el.append(h("p", { class: "msg__note is-error", text: m.error }))
|
||||
if (m.stopped) el.append(h("p", { class: "msg__note", text: t("Stopped.") }))
|
||||
const meta = metricsLine(m.metrics)
|
||||
if (meta || m.model) {
|
||||
el.append(h("footer", { class: "msg__foot" }, m.model ? h("span", { class: "chip", text: m.model }) : null, meta))
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
function renderSteps(box, steps, chat) {
|
||||
clear(box)
|
||||
for (const s of steps) box.append(step(s, chat))
|
||||
}
|
||||
|
||||
function step(s, chat) {
|
||||
switch (s.kind) {
|
||||
case "thinking":
|
||||
return h(
|
||||
"details",
|
||||
{ class: "step step--thinking", open: s.open || null },
|
||||
h("summary", null, t("Thought"), s.label ? h("span", { class: "muted", text: ` ${s.label}` }) : null),
|
||||
h("div", { class: "step__pre", text: s.text || "" }),
|
||||
)
|
||||
case "text":
|
||||
return h("div", { class: "step step--text md" }, markdown(s.markdown || ""))
|
||||
case "tools":
|
||||
return h("div", { class: "step step--tools" }, (s.calls || []).map((c) => toolCall(c, chat)))
|
||||
case "steer":
|
||||
return h(
|
||||
"div",
|
||||
{ class: "step step--steer" },
|
||||
h("p", { class: "muted", text: t("Taken in while it ran:") }),
|
||||
(s.messages || []).map((m) => h("p", { class: "steer__text", text: m.text })),
|
||||
)
|
||||
default:
|
||||
return h("div", { class: "step" })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the text of a file the agent showed comes from, or null when there is none to fetch:
|
||||
* - an agent chat on this computer: the file itself, read here (the card's path, or the call's
|
||||
* own `args.path` for a step the card does not describe);
|
||||
* - otherwise the server, through the card (`show`): `?node=&version=` for a file in the tree,
|
||||
* `?shown=` for the copy a device sent. The server checks the chat's access again then.
|
||||
*/
|
||||
function shownSource(call, chat) {
|
||||
if (onThisComputer(chat) && chat.device.directory) {
|
||||
const path = call.show?.path || call.args?.path || ""
|
||||
return path ? { local: true, path } : null
|
||||
}
|
||||
const s = call.show
|
||||
if (!s || !chat?.id) return null
|
||||
const q = s.shown_id
|
||||
? `shown=${encodeURIComponent(s.shown_id)}`
|
||||
: s.node_id && s.version_id
|
||||
? `node=${encodeURIComponent(s.node_id)}&version=${encodeURIComponent(s.version_id)}`
|
||||
: ""
|
||||
return q ? { local: false, path: s.path || "", url: `/chats/${encodeURIComponent(chat.id)}/show?${q}` } : null
|
||||
}
|
||||
|
||||
function toolCall(c, chat) {
|
||||
const block = c.block || {}
|
||||
const failed = c.status === "error" || !!c.error
|
||||
const body = h("div", { class: "call__body" })
|
||||
if (c.purpose) body.append(h("p", { class: "muted", text: c.purpose }))
|
||||
if (c.args?.command) body.append(h("pre", { class: "call__pre", text: c.args.command }))
|
||||
if (c.diff) body.append(h("pre", { class: "call__pre call__diff", text: typeof c.diff === "string" ? c.diff : JSON.stringify(c.diff, null, 2) }))
|
||||
if (c.text) body.append(h("pre", { class: "call__pre", text: c.text }))
|
||||
if (c.error) body.append(h("p", { class: "msg__note is-error", text: typeof c.error === "string" ? c.error : JSON.stringify(c.error) }))
|
||||
if (c.truncated) body.append(h("p", { class: "muted", text: t("Cut short here; the whole of it is in the web interface.") }))
|
||||
const isShow = c.name === "show" && !failed && shownSource(c, chat)
|
||||
const summary = h(
|
||||
"summary",
|
||||
{ class: "call__head" },
|
||||
h("span", { class: `dot${failed ? " is-error" : c.status === "running" ? " is-busy" : " is-ok"}`, "aria-hidden": "true" }),
|
||||
h("span", { class: "call__title", title: block.title || c.name, text: block.title || c.name || "" }),
|
||||
h("span", { class: "call__chips" }, (block.chips || []).map((x) => h("span", { class: "chip", text: x }))),
|
||||
)
|
||||
const el = h("details", { class: `call${failed ? " is-failed" : ""}` }, summary, body)
|
||||
if (isShow) {
|
||||
el.append(h("button", { type: "button", class: "btn btn--sm call__open", text: t("Open in the viewer"), onclick: () => openShown(c, chat) }))
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
/** A file the agent showed: from this computer when the chat runs here, else from the server. */
|
||||
async function openShown(call, chat) {
|
||||
const src = shownSource(call, chat)
|
||||
if (!src) return
|
||||
const path = src.path
|
||||
const { language } = showKind(path)
|
||||
const line = call.show?.line || (src.local ? call.args?.line : 0) || 0
|
||||
const base = { name: call.show?.name || path.split("/").pop(), path, kind: call.show?.kind, language: call.show?.language || language, line }
|
||||
try {
|
||||
if (src.local) {
|
||||
const dir = chat.device.directory.replace(/\/+$/, "")
|
||||
const rel = path.startsWith(`${dir}/`) ? path.slice(dir.length + 1) : path
|
||||
const f = await readLocal(dir, rel)
|
||||
openViewer({ ...base, where: `${t("Shown by the agent")} · ${t("This computer")}`, text: f.text, withheld: f.withheld, size: f.size })
|
||||
} else {
|
||||
// Plain text, at most 1 MB; the server's 404 (no longer the chat's), 413 (too large) and
|
||||
// 415 (not text) come back as its own words. HTML is its source: the viewer seals it.
|
||||
const text = await getText(src.url)
|
||||
openViewer({ ...base, where: `${t("Shown by the agent")} · ${t("Server")}`, text })
|
||||
}
|
||||
} catch (err) {
|
||||
openViewer({ ...base, where: t("Shown by the agent"), error: why(err) })
|
||||
}
|
||||
}
|
||||
|
||||
// --- The reply being written ---------------------------------------------------------------------
|
||||
|
||||
class Live {
|
||||
constructor(chat, messageId) {
|
||||
this.chat = chat
|
||||
this.id = messageId
|
||||
this.steps = h("div", { class: "msg__steps" })
|
||||
this.tail = h("div", { class: "step step--text md msg__tail" })
|
||||
this.thinking = h("p", { class: "msg__thinking muted" })
|
||||
this.status = h("p", { class: "msg__status muted", text: t("Writing…") })
|
||||
this.jobs = h("ul", { class: "msg__jobs", hidden: true })
|
||||
this.el = h("article", { class: "msg msg--assistant is-live", dataset: { id: messageId } }, this.steps, this.thinking, this.tail, this.jobs, this.status)
|
||||
this.done = false
|
||||
this.handle = null
|
||||
}
|
||||
|
||||
async start() {
|
||||
try {
|
||||
this.handle = await follow(`/chats/${encodeURIComponent(this.chat.id)}/messages/${encodeURIComponent(this.id)}/stream`, {
|
||||
onFrame: (event, data) => this.frame(event, data),
|
||||
onEnd: (err) => this.end(err),
|
||||
})
|
||||
} catch (err) {
|
||||
this.end(err)
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.done = true
|
||||
this.handle?.close()
|
||||
}
|
||||
|
||||
frame(event, data) {
|
||||
if (this.done) return
|
||||
switch (event) {
|
||||
case "steps":
|
||||
renderSteps(this.steps, data.steps || [], this.chat)
|
||||
this.tailed(data.tail)
|
||||
break
|
||||
case "tail":
|
||||
this.tailed(data)
|
||||
break
|
||||
case "status":
|
||||
this.status.textContent = data.text || t("Writing…")
|
||||
break
|
||||
case "ask":
|
||||
renderCards(this.chat, data.cards || [])
|
||||
break
|
||||
case "jobs":
|
||||
this.jobs.hidden = !(data.jobs || []).length
|
||||
append(
|
||||
clear(this.jobs),
|
||||
(data.jobs || []).map((j) =>
|
||||
h("li", { class: `job job--${j.state}` }, h("span", { class: "chip", text: j.state }), h("span", { text: j.description || j.command || j.agent || "" })),
|
||||
),
|
||||
)
|
||||
break
|
||||
case "steered":
|
||||
for (const id of data.ids || []) document.querySelector(`.msg--user[data-id="${CSS.escape(id)}"]`)?.classList.remove("is-queued")
|
||||
break
|
||||
case "done":
|
||||
this.done = true
|
||||
this.el.replaceWith(assistantMessage(data.message, this.chat))
|
||||
renderCards(this.chat, [])
|
||||
if (data.chat?.title) setTitle(data.chat.title)
|
||||
ctx.changed(this.chat)
|
||||
if (open?.live === this) open.live = null
|
||||
setRunning(false)
|
||||
// A message queued behind this reply has one of its own now: read the chat again.
|
||||
if (data.next) reload()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
toBottom(false)
|
||||
}
|
||||
|
||||
tailed(tail) {
|
||||
if (!tail) return
|
||||
this.thinking.textContent = tail.thinking ? `${t("Thinking")} ${tail.label || ""}…` : ""
|
||||
this.thinking.hidden = !tail.thinking
|
||||
clear(this.tail).append(markdown(tail.text || ""))
|
||||
}
|
||||
|
||||
end(err) {
|
||||
if (this.done) return
|
||||
this.done = true
|
||||
if (open?.live === this) open.live = null
|
||||
setRunning(false)
|
||||
if (err) {
|
||||
this.status.textContent = t("The connection to the reply was lost: %(why)s", { why: why(err) })
|
||||
this.status.classList.add("is-error")
|
||||
this.el.append(h("button", { type: "button", class: "btn btn--sm", text: t("Read again"), onclick: reload }))
|
||||
} else reload()
|
||||
}
|
||||
}
|
||||
|
||||
function setRunning(on) {
|
||||
$("send").hidden = on && !$("composer-text").value.trim()
|
||||
$("stop").hidden = !on
|
||||
$("composer").dataset.running = String(on)
|
||||
}
|
||||
|
||||
function follower(chat, messageId) {
|
||||
if (open?.live) open.live.stop()
|
||||
const live = new Live(chat, messageId)
|
||||
open.live = live
|
||||
$("messages").append(live.el)
|
||||
setRunning(true)
|
||||
live.start()
|
||||
toBottom(true)
|
||||
}
|
||||
|
||||
// --- Cards ----------------------------------------------------------------------------------------
|
||||
|
||||
function renderCards(chat, cards) {
|
||||
const box = clear($("cards"))
|
||||
box.hidden = !cards.length
|
||||
for (const card of cards) box.append(cardOf(chat, card))
|
||||
}
|
||||
|
||||
function cardOf(chat, card) {
|
||||
const answers = { choices: {}, texts: {} }
|
||||
const decides = card.items.some((i) => i.kind === "approval" || i.kind === "plan")
|
||||
const el = h("section", { class: `ask ask--${card.kind}`, "aria-label": t("Waiting for your answer") })
|
||||
if (card.why) el.append(h("p", { class: "muted", text: card.why }))
|
||||
for (const item of card.items) {
|
||||
const part = h("div", { class: "ask__item" })
|
||||
if (item.header) part.append(h("p", { class: "ask__header", text: item.header }))
|
||||
if (item.kind === "approval") {
|
||||
part.append(h("p", { class: "ask__title", text: item.tool ? t("The agent asks to run %(tool)s:", { tool: item.tool }) : t("The agent asks:") }))
|
||||
if (item.editable) {
|
||||
const edit = h("textarea", { class: "ask__edit", rows: "2", "aria-label": t("Command"), value: item.title })
|
||||
edit.addEventListener("input", () => {
|
||||
if (edit.value !== item.title) answers.texts[item.key] = edit.value
|
||||
else delete answers.texts[item.key]
|
||||
})
|
||||
part.append(edit)
|
||||
} else part.append(h("pre", { class: "call__pre", text: item.title }))
|
||||
} else if (item.kind === "plan" && item.plan) {
|
||||
part.append(h("p", { class: "ask__title", text: item.plan.title || item.title }), planList(item.plan))
|
||||
} else {
|
||||
part.append(h("p", { class: "ask__title", text: item.title }))
|
||||
}
|
||||
if (item.detail) part.append(h("p", { class: "muted", text: item.detail }))
|
||||
if (item.purpose) part.append(h("p", { class: "muted", text: item.purpose }))
|
||||
if (item.options?.length) {
|
||||
const picked = new Set()
|
||||
const opts = h("div", { class: "ask__options", role: item.multiple ? "group" : "radiogroup" })
|
||||
for (const o of item.options) {
|
||||
const b = h(
|
||||
"button",
|
||||
{ type: "button", class: "opt", "aria-pressed": "false" },
|
||||
h("span", { class: "opt__label", text: o.label }),
|
||||
o.description ? h("span", { class: "opt__desc", text: o.description }) : null,
|
||||
o.recommended ? h("span", { class: "chip", text: t("Recommended") }) : null,
|
||||
)
|
||||
b.addEventListener("click", () => {
|
||||
if (!item.multiple) {
|
||||
picked.clear()
|
||||
for (const x of opts.children) x.setAttribute("aria-pressed", "false")
|
||||
}
|
||||
if (picked.has(o.label)) picked.delete(o.label)
|
||||
else picked.add(o.label)
|
||||
b.setAttribute("aria-pressed", String(picked.has(o.label)))
|
||||
answers.choices[item.key] = [...picked]
|
||||
})
|
||||
opts.append(b)
|
||||
}
|
||||
part.append(opts)
|
||||
}
|
||||
if (item.free_text && item.kind === "question") {
|
||||
const typed = h("input", { type: "text", class: "ask__text", placeholder: t("Or type an answer"), "aria-label": t("Your answer") })
|
||||
typed.addEventListener("input", () => {
|
||||
answers.texts[item.key] = typed.value
|
||||
})
|
||||
part.append(typed)
|
||||
}
|
||||
el.append(part)
|
||||
}
|
||||
const said = h("p", { class: "ask__said", "aria-live": "polite" })
|
||||
const send = async (verdict) => {
|
||||
for (const b of el.querySelectorAll(".ask__actions button")) b.disabled = true
|
||||
try {
|
||||
await post(`/chats/${encodeURIComponent(chat.id)}/interactions/${encodeURIComponent(card.id)}`, {
|
||||
verdict,
|
||||
choices: answers.choices,
|
||||
texts: answers.texts,
|
||||
})
|
||||
el.remove()
|
||||
if (!$("cards").children.length) $("cards").hidden = true
|
||||
} catch (err) {
|
||||
if (err?.status === 409) {
|
||||
el.remove()
|
||||
reload()
|
||||
return
|
||||
}
|
||||
said.textContent = why(err)
|
||||
for (const b of el.querySelectorAll(".ask__actions button")) b.disabled = false
|
||||
}
|
||||
}
|
||||
const actions = h("div", { class: "ask__actions" })
|
||||
if (decides) {
|
||||
const isPlan = card.items.some((i) => i.kind === "plan")
|
||||
append(
|
||||
actions,
|
||||
h("button", { type: "button", class: "btn btn--primary", text: isPlan ? t("Carry it out") : t("Allow"), onclick: () => send("allow") }),
|
||||
isPlan ? null : h("button", { type: "button", class: "btn", text: t("Always allow"), onclick: () => send("allow_always") }),
|
||||
h("button", { type: "button", class: "btn", text: isPlan ? t("Not yet") : t("Deny"), onclick: () => send("deny") }),
|
||||
)
|
||||
} else {
|
||||
actions.append(h("button", { type: "button", class: "btn btn--primary", text: t("Answer"), onclick: () => send("") }))
|
||||
}
|
||||
el.append(actions, said)
|
||||
return el
|
||||
}
|
||||
|
||||
function planList(plan) {
|
||||
return h(
|
||||
"ol",
|
||||
{ class: "plan" },
|
||||
(plan.phases || []).map((p) =>
|
||||
h("li", null, h("span", { class: "plan__phase", text: p.title }), h("ul", null, (p.tasks || []).map((task) => h("li", { class: `plan__task is-${task.status}`, text: task.text })))),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// --- Opening a chat --------------------------------------------------------------------------------
|
||||
|
||||
function setTitle(title) {
|
||||
$("chat-title").textContent = title || t("New chat")
|
||||
$("chat-title").title = title || ""
|
||||
}
|
||||
|
||||
function renderHead(chat) {
|
||||
setTitle(chat.title)
|
||||
const place = clear($("chat-place"))
|
||||
const tools = clear($("chat-tools"))
|
||||
if (chat.kind === "agent" && chat.device) {
|
||||
const here = onThisComputer(chat)
|
||||
place.append(
|
||||
h("span", { class: `chip${here ? " chip--leaf" : ""}`, text: here ? t("This computer") : t("Another device") }),
|
||||
h("span", { class: "head__dir", title: chat.device.directory }, h("bdi", { text: chat.device.directory })),
|
||||
)
|
||||
const mode = h(
|
||||
"select",
|
||||
{ class: "head__mode", "aria-label": t("Mode"), onchange: (e) => patch(`/chats/${encodeURIComponent(chat.id)}`, { mode: e.target.value }).catch((err) => alertIn(why(err))) },
|
||||
MODES.map((m) => h("option", { value: m, text: modeName(m), selected: chat.mode === m })),
|
||||
)
|
||||
tools.append(mode)
|
||||
} else if (chat.model) {
|
||||
place.append(h("span", { class: "chip", text: chat.model }))
|
||||
}
|
||||
}
|
||||
|
||||
function modeName(m) {
|
||||
return { manual: "Manual", edit: "Edit", auto: "Auto", plan: "Plan" }[m] || m
|
||||
}
|
||||
|
||||
function alertIn(text) {
|
||||
const box = $("thread-note")
|
||||
box.textContent = text
|
||||
box.hidden = !text
|
||||
}
|
||||
|
||||
/** Open a chat by id: its newest page, and the reply being written, if one is. */
|
||||
export async function openChat(id) {
|
||||
if (open?.live) open.live.stop()
|
||||
open = { chat: { id }, live: null, more: false }
|
||||
invoke("set_open_chat", { chat: id })
|
||||
showComposer(true)
|
||||
$("new-chat").hidden = true
|
||||
const list = clear($("messages"))
|
||||
renderCards({ id }, [])
|
||||
alertIn("")
|
||||
list.append(h("p", { class: "muted thread__loading", text: t("Loading…") }))
|
||||
let r
|
||||
try {
|
||||
r = await get(`/chats/${encodeURIComponent(id)}`)
|
||||
} catch (err) {
|
||||
clear(list)
|
||||
alertIn(why(err))
|
||||
return
|
||||
}
|
||||
if (open?.chat?.id !== id) return
|
||||
open.chat = r.chat
|
||||
open.more = r.more
|
||||
renderHead(r.chat)
|
||||
clear(list)
|
||||
if (r.more) list.append(earlier())
|
||||
for (const m of r.messages) {
|
||||
if (m.id === r.live) continue
|
||||
list.append(m.role === "user" ? userMessage(m) : assistantMessage(m, r.chat))
|
||||
}
|
||||
ctx.opened(r.chat)
|
||||
if (r.live) follower(r.chat, r.live)
|
||||
else setRunning(false)
|
||||
toBottom(true)
|
||||
$("composer-text").focus()
|
||||
}
|
||||
|
||||
function earlier() {
|
||||
return h("button", {
|
||||
type: "button",
|
||||
class: "btn btn--sm thread__earlier",
|
||||
text: t("Earlier messages"),
|
||||
onclick: async (e) => {
|
||||
const first = $("messages").querySelector(".msg")?.dataset.id
|
||||
if (!first || !open) return
|
||||
e.target.disabled = true
|
||||
try {
|
||||
const r = await get(`/chats/${encodeURIComponent(open.chat.id)}/messages?before=${encodeURIComponent(first)}`)
|
||||
const th = $("thread")
|
||||
const from = th.scrollHeight
|
||||
const frag = document.createDocumentFragment()
|
||||
if (r.more) frag.append(earlier())
|
||||
for (const m of r.messages) frag.append(m.role === "user" ? userMessage(m) : assistantMessage(m, open.chat))
|
||||
e.target.replaceWith(frag)
|
||||
th.scrollTop += th.scrollHeight - from
|
||||
} catch (err) {
|
||||
e.target.disabled = false
|
||||
alertIn(why(err))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (open?.chat?.id) openChat(open.chat.id)
|
||||
}
|
||||
|
||||
export function closeChat() {
|
||||
if (open?.live) open.live.stop()
|
||||
open = null
|
||||
invoke("set_open_chat", { chat: null })
|
||||
}
|
||||
|
||||
// --- A new chat ------------------------------------------------------------------------------------
|
||||
|
||||
let draft = null // { kind }
|
||||
|
||||
/** The form a new chat starts from: `agent` (on this computer) or `chat` (on the server). */
|
||||
export function newChat(kind) {
|
||||
closeChat()
|
||||
draft = { kind }
|
||||
clear($("messages"))
|
||||
renderCards({ id: "" }, [])
|
||||
alertIn("")
|
||||
setTitle(kind === "agent" ? t("New agent chat") : t("New chat"))
|
||||
clear($("chat-place"))
|
||||
clear($("chat-tools"))
|
||||
const form = clear($("new-chat"))
|
||||
form.hidden = false
|
||||
const s = ctx.status()
|
||||
const models = ctx.models()
|
||||
const modelField = field(
|
||||
t("Model"),
|
||||
h(
|
||||
"select",
|
||||
{ id: "new-model" },
|
||||
models.map((m) => h("option", { value: m.id, text: m.name, selected: m.default })),
|
||||
),
|
||||
models.length ? "" : t("No model is available to this account."),
|
||||
)
|
||||
if (kind === "agent") {
|
||||
const roots = s.remote?.enabled ? s.remote.roots : []
|
||||
const devices = ctx.thisDevices()
|
||||
form.append(
|
||||
h("p", { class: "muted", text: t("An agent chat works in a folder on this computer, through the agent built into this app.") }),
|
||||
)
|
||||
if (!devices.length) {
|
||||
form.append(
|
||||
h("p", { class: "new__warn", text: t("The agent is not connected to the server yet: start it, or link this computer, under Settings.") }),
|
||||
)
|
||||
}
|
||||
form.append(
|
||||
h(
|
||||
"div",
|
||||
{ class: "fields" },
|
||||
field(
|
||||
t("Folder"),
|
||||
h("select", { id: "new-dir" }, roots.map((r) => h("option", { value: r, text: r }))),
|
||||
roots.length ? t("Only the agent's folders: Settings, Project folders.") : t("No folder yet: add one under Settings, Project folders."),
|
||||
),
|
||||
field(
|
||||
t("Mode"),
|
||||
h(
|
||||
"select",
|
||||
{ id: "new-mode" },
|
||||
MODES.map((m) => h("option", { value: m, text: modeName(m), selected: m === "edit" })),
|
||||
),
|
||||
t("A chat asking for more than the folder allows is lowered to it."),
|
||||
),
|
||||
modelField,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
form.append(h("div", { class: "fields" }, modelField))
|
||||
}
|
||||
showComposer(true)
|
||||
setRunning(false)
|
||||
invoke("set_open_chat", { chat: null })
|
||||
$("composer-text").focus()
|
||||
}
|
||||
|
||||
/** A field: label, control, hint -- always all three, so a row of them lines up. */
|
||||
function field(label, control, hint) {
|
||||
const id = control.id || `f${Math.random().toString(36).slice(2)}`
|
||||
control.id = id
|
||||
return h("div", { class: "field" }, h("label", { for: id, text: label }), control, h("p", { class: "hint", text: hint || "" }))
|
||||
}
|
||||
|
||||
async function createChat(text) {
|
||||
const body = { text }
|
||||
const model = $("new-model")?.value
|
||||
if (model) body.model = model
|
||||
if (draft.kind === "agent") {
|
||||
const device = ctx.thisDevices()[0]
|
||||
const dir = $("new-dir")?.value
|
||||
if (!device) throw { detail: t("The agent is not connected to the server yet: start it, or link this computer, under Settings.") }
|
||||
if (!dir) throw { detail: t("No folder yet: add one under Settings, Project folders.") }
|
||||
Object.assign(body, { kind: "agent", device, directory: dir, mode: $("new-mode")?.value || "edit" })
|
||||
}
|
||||
const r = await post("/chats", body)
|
||||
draft = null
|
||||
$("new-chat").hidden = true
|
||||
open = { chat: r.chat, live: null, more: false }
|
||||
invoke("set_open_chat", { chat: r.chat.id })
|
||||
renderHead(r.chat)
|
||||
clear($("messages"))
|
||||
if (r.message) $("messages").append(userMessage(r.message))
|
||||
ctx.changed(r.chat)
|
||||
ctx.opened(r.chat)
|
||||
if (r.reply) follower(r.chat, r.reply.id)
|
||||
}
|
||||
|
||||
// --- The composer ------------------------------------------------------------------------------------
|
||||
|
||||
function showComposer(on) {
|
||||
$("composer").hidden = !on
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const box = $("composer-text")
|
||||
const text = box.value.trim()
|
||||
if (!text) return
|
||||
$("send").disabled = true
|
||||
alertIn("")
|
||||
try {
|
||||
if (draft) await createChat(text)
|
||||
else if (open?.chat?.id) {
|
||||
const r = await post(`/chats/${encodeURIComponent(open.chat.id)}/messages`, { text })
|
||||
if (r.message) $("messages").append(userMessage(r.message))
|
||||
if (r.reply) follower(open.chat, r.reply.id)
|
||||
}
|
||||
box.value = ""
|
||||
grow()
|
||||
toBottom(true)
|
||||
} catch (err) {
|
||||
alertIn(why(err))
|
||||
} finally {
|
||||
$("send").disabled = false
|
||||
setRunning(!!open?.live)
|
||||
}
|
||||
}
|
||||
|
||||
function grow() {
|
||||
const box = $("composer-text")
|
||||
box.style.height = "auto"
|
||||
box.style.height = `${Math.min(box.scrollHeight, window.innerHeight * 0.4)}px`
|
||||
}
|
||||
|
||||
export function initChat(context) {
|
||||
ctx = context
|
||||
$("composer").addEventListener("submit", (e) => {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
})
|
||||
$("composer-text").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}
|
||||
})
|
||||
$("composer-text").addEventListener("input", () => {
|
||||
grow()
|
||||
if (open?.live) $("send").hidden = !$("composer-text").value.trim()
|
||||
})
|
||||
$("stop").addEventListener("click", async () => {
|
||||
const live = open?.live
|
||||
if (!live) return
|
||||
try {
|
||||
await api("POST", `/chats/${encodeURIComponent(live.chat.id)}/messages/${encodeURIComponent(live.id)}/stop`)
|
||||
} catch (err) {
|
||||
alertIn(why(err))
|
||||
}
|
||||
})
|
||||
showComposer(false)
|
||||
}
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Source in the viewer: CodeMirror, read-only, from the server's own vendored build (ui/vendor,
|
||||
// copied and pinned by scripts/fetch-vendor.sh). The look is the server's editor's: colours from
|
||||
// the theme's tokens, never CodeMirror's own. Loaded the first time a file is shown.
|
||||
|
||||
let cm = null
|
||||
|
||||
async function load() {
|
||||
if (cm) return cm
|
||||
const [state, view, language, search, commands, highlight] = await Promise.all([
|
||||
import("../vendor/cm-state.mjs"),
|
||||
import("../vendor/cm-view.mjs"),
|
||||
import("../vendor/cm-language.mjs"),
|
||||
import("../vendor/cm-search.mjs"),
|
||||
import("../vendor/cm-commands.mjs"),
|
||||
import("../vendor/lezer-highlight.mjs"),
|
||||
])
|
||||
const { EditorView } = view
|
||||
const { HighlightStyle, syntaxHighlighting } = language
|
||||
const { tags } = highlight
|
||||
// As the server's static/js/editor.js has them.
|
||||
const look = EditorView.theme({
|
||||
"&": { color: "var(--ink)", backgroundColor: "var(--code-bg)", fontSize: "var(--text-sm)", height: "100%" },
|
||||
".cm-scroller": { fontFamily: "var(--font-mono)", lineHeight: "var(--leading-normal)" },
|
||||
".cm-content": { caretColor: "var(--accent)" },
|
||||
".cm-gutters": { backgroundColor: "var(--code-bg)", color: "var(--ink-faint)", border: "none" },
|
||||
".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "var(--surface-hover)" },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { backgroundColor: "var(--selection)" },
|
||||
"&.cm-focused": { outline: "none" },
|
||||
".cm-panels": { backgroundColor: "var(--surface)", color: "var(--ink)" },
|
||||
".cm-searchMatch": { backgroundColor: "var(--accent-soft)" },
|
||||
})
|
||||
const colours = HighlightStyle.define([
|
||||
{ tag: [tags.keyword, tags.operatorKeyword, tags.modifier], color: "var(--accent)" },
|
||||
{ tag: [tags.string, tags.special(tags.string)], color: "var(--leaf)" },
|
||||
{ tag: [tags.comment], color: "var(--ink-faint)", fontStyle: "italic" },
|
||||
{ tag: [tags.number, tags.bool, tags.null, tags.atom], color: "var(--warning)" },
|
||||
{ tag: [tags.function(tags.variableName), tags.definition(tags.variableName)], color: "var(--ink)", fontWeight: "600" },
|
||||
{ tag: [tags.heading], color: "var(--accent)", fontWeight: "700" },
|
||||
{ tag: [tags.link, tags.url], color: "var(--accent)", textDecoration: "underline" },
|
||||
{ tag: [tags.tagName, tags.typeName, tags.className], color: "var(--accent-hover)" },
|
||||
{ tag: [tags.invalid], color: "var(--danger)" },
|
||||
])
|
||||
cm = {
|
||||
state,
|
||||
view,
|
||||
base: [
|
||||
view.lineNumbers(),
|
||||
view.highlightActiveLine(),
|
||||
view.highlightActiveLineGutter(),
|
||||
view.drawSelection(),
|
||||
view.keymap.of([...search.searchKeymap, ...commands.defaultKeymap]),
|
||||
search.highlightSelectionMatches(),
|
||||
state.EditorState.readOnly.of(true),
|
||||
view.EditorView.editable.of(false),
|
||||
view.EditorView.lineWrapping,
|
||||
look,
|
||||
syntaxHighlighting(colours),
|
||||
],
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
/** The language support for a name as `show` gives it; nothing for one without support. */
|
||||
async function language(name) {
|
||||
try {
|
||||
switch (name) {
|
||||
case "markdown":
|
||||
return (await import("../vendor/cm-lang-markdown.mjs")).markdown()
|
||||
case "python":
|
||||
return (await import("../vendor/cm-lang-python.mjs")).python()
|
||||
case "javascript":
|
||||
case "jsx":
|
||||
return (await import("../vendor/cm-lang-javascript.mjs")).javascript({ jsx: name === "jsx" })
|
||||
case "typescript":
|
||||
case "tsx":
|
||||
return (await import("../vendor/cm-lang-javascript.mjs")).javascript({ typescript: true, jsx: name === "tsx" })
|
||||
case "html":
|
||||
return (await import("../vendor/cm-lang-html.mjs")).html()
|
||||
case "css":
|
||||
case "scss":
|
||||
return (await import("../vendor/cm-lang-css.mjs")).css()
|
||||
case "json":
|
||||
return (await import("../vendor/cm-lang-json.mjs")).json()
|
||||
default:
|
||||
return []
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Show `text` in `parent`, read-only, at `line` when given. Returns something to `destroy()`. */
|
||||
export async function codeView(parent, text, { lang = "", line = 0 } = {}) {
|
||||
const { state, view, base } = await load()
|
||||
const ed = new view.EditorView({
|
||||
parent,
|
||||
state: state.EditorState.create({ doc: text, extensions: [base, await language(lang)] }),
|
||||
})
|
||||
if (line > 0 && line <= ed.state.doc.lines) {
|
||||
const at = ed.state.doc.line(line).from
|
||||
ed.dispatch({ selection: { anchor: at }, effects: view.EditorView.scrollIntoView(at, { y: "center" }) })
|
||||
}
|
||||
return ed
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Building the page: elements made in code, text always as text. Nothing here ever parses a
|
||||
// string as HTML -- what the server sends (a reply, a file, a title) is model output or the
|
||||
// person's own words, and is set with textContent, never innerHTML.
|
||||
|
||||
export const $ = (id) => document.getElementById(id)
|
||||
|
||||
/**
|
||||
* h("button", { class: "btn", onclick: fn, "aria-label": "…" }, "text", child, [more])
|
||||
* Attributes: `class`, `text`, `hidden`, `on<event>` (a function), `dataset` (an object), any
|
||||
* other as an attribute (false/null/undefined leave it out, true sets it empty).
|
||||
*/
|
||||
export function h(tag, attrs, ...children) {
|
||||
const el = document.createElement(tag)
|
||||
for (const [key, value] of Object.entries(attrs || {})) {
|
||||
if (value === null || value === undefined || value === false) continue
|
||||
if (key === "class") el.className = value
|
||||
else if (key === "text") el.textContent = value
|
||||
else if (key === "dataset") Object.assign(el.dataset, value)
|
||||
else if (key.startsWith("on") && typeof value === "function") el.addEventListener(key.slice(2), value)
|
||||
else if (key === "hidden") el.hidden = !!value
|
||||
else if (key === "value") el.value = value
|
||||
else if (key === "checked") el.checked = !!value
|
||||
else if (key === "disabled") el.disabled = !!value
|
||||
else el.setAttribute(key, value === true ? "" : String(value))
|
||||
}
|
||||
append(el, children)
|
||||
return el
|
||||
}
|
||||
|
||||
export function append(el, children) {
|
||||
for (const c of children.flat(Infinity)) {
|
||||
if (c === null || c === undefined || c === false) continue
|
||||
el.append(c instanceof Node ? c : document.createTextNode(String(c)))
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
export function clear(el) {
|
||||
el.replaceChildren()
|
||||
return el
|
||||
}
|
||||
|
||||
/** An inline SVG icon from a path list, drawn in currentColor. */
|
||||
export function icon(d, label) {
|
||||
const ns = "http://www.w3.org/2000/svg"
|
||||
const svg = document.createElementNS(ns, "svg")
|
||||
svg.setAttribute("viewBox", "0 0 16 16")
|
||||
svg.setAttribute("aria-hidden", "true")
|
||||
svg.classList.add("icon")
|
||||
for (const one of [].concat(d)) {
|
||||
const p = document.createElementNS(ns, "path")
|
||||
p.setAttribute("d", one)
|
||||
p.setAttribute("fill-rule", "evenodd")
|
||||
svg.append(p)
|
||||
}
|
||||
if (label) {
|
||||
svg.removeAttribute("aria-hidden")
|
||||
svg.setAttribute("role", "img")
|
||||
svg.setAttribute("aria-label", label)
|
||||
}
|
||||
return svg
|
||||
}
|
||||
|
||||
export const ICONS = {
|
||||
plus: "M7.25 3h1.5v4.25H13v1.5H8.75V13h-1.5V8.75H3v-1.5h4.25z",
|
||||
close: "M4.5 3.4L8 6.9l3.5-3.5 1.1 1.1L9.1 8l3.5 3.5-1.1 1.1L8 9.1l-3.5 3.5-1.1-1.1L6.9 8 3.4 4.5z",
|
||||
folder: "M1.5 3h5l1.5 1.5h6.5v8.5h-13zM3 6v5.5h10V6z",
|
||||
file: "M3 1.5h6.5L13 5v9.5H3zM4.5 3v10h7V6H8.5V3z",
|
||||
chevron: "M6 3.5L10.5 8 6 12.5 4.9 11.4 8.3 8 4.9 4.6z",
|
||||
menu: "M3 4h10v1.5H3zM3 7.25h10v1.5H3zM3 10.5h10V12H3z",
|
||||
side: "M2 3h12v10H2zM3.5 4.5v7H6v-7z",
|
||||
gear: "M7 1.5h2l.4 1.8 1.1.5 1.6-1 1.4 1.4-1 1.6.5 1.1 1.8.4v2l-1.8.4-.5 1.1 1 1.6-1.4 1.4-1.6-1-1.1.5L9 14.5H7l-.4-1.8-1.1-.5-1.6 1-1.4-1.4 1-1.6-.5-1.1-1.8-.4v-2l1.8-.4.5-1.1-1-1.6 1.4-1.4 1.6 1 1.1-.5zM8 5.75a2.25 2.25 0 100 4.5 2.25 2.25 0 000-4.5z",
|
||||
refresh: "M8 2.5a5.5 5.5 0 015.2 3.7H11.5v1.5H15V4.2h-1.5v1A7 7 0 001 8h1.5A5.5 5.5 0 018 2.5zM2.5 11.8v-1h1.7A5.5 5.5 0 0013.5 8H15A7 7 0 012.5 11.8z",
|
||||
min: "M4 10h8v1.5H4z",
|
||||
max: "M4 4h8v8H4zM5.5 5.5v5h5v-5z",
|
||||
restore: "M5.5 3h7.5v7.5h-2.5V13H3V5.5h2.5zM7 4.5v1h3.5V9h1V4.5zM4.5 7v4.5H9V7z",
|
||||
}
|
||||
|
||||
/** "3 min ago"-style, in the system's language, for a stamp the server sent. */
|
||||
export function when(stamp) {
|
||||
if (!stamp) return ""
|
||||
const then = new Date(stamp)
|
||||
if (Number.isNaN(then.getTime())) return ""
|
||||
const seconds = Math.round((then.getTime() - Date.now()) / 1000)
|
||||
const rtf = new Intl.RelativeTimeFormat(document.documentElement.lang || undefined, { numeric: "auto" })
|
||||
const steps = [
|
||||
[60, "second"],
|
||||
[60, "minute"],
|
||||
[24, "hour"],
|
||||
[7, "day"],
|
||||
[4.35, "week"],
|
||||
[12, "month"],
|
||||
[Infinity, "year"],
|
||||
]
|
||||
let value = seconds
|
||||
for (const [size, unit] of steps) {
|
||||
if (Math.abs(value) < size) return rtf.format(Math.round(value), unit)
|
||||
value /= size
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/** A size in bytes, as a person reads it. */
|
||||
export function bytes(n) {
|
||||
if (!Number.isFinite(n)) return ""
|
||||
const units = ["B", "KB", "MB", "GB"]
|
||||
let i = 0
|
||||
let v = n
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024
|
||||
i += 1
|
||||
}
|
||||
return `${i === 0 ? v : v.toFixed(v < 10 ? 1 : 0)} ${units[i]}`
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// The two explorers, kept apart: this computer's -- the agent's working directory, read through
|
||||
// a command that reaches only the agent's folders (src-tauri/src/files.rs) -- and the server's
|
||||
// tree, for the chats, read through /api/v2. A file opens in the viewer either way.
|
||||
|
||||
import { $, h, clear, icon, ICONS, bytes } from "./dom.js"
|
||||
import { invoke, get, why } from "./bridge.js"
|
||||
import { openViewer } from "./viewer.js"
|
||||
|
||||
// The server's tree as the app's door gives it, read as the person (their own access, no chat):
|
||||
// GET /files?path=/a -> { path, parent, entries: [{ name, path, kind: "dir"|"file"|"mount", size, mime, updated_at }] }
|
||||
// GET /files/content?path= -> { path, name, size, mime, kind, language, sha, text, withheld?: { reason, message } }
|
||||
// A path the person cannot read is the server's 404, in its words.
|
||||
export const SERVER_FILES = {
|
||||
list: (path) => `/files?path=${encodeURIComponent(path || "/")}`,
|
||||
read: (path) => `/files/content?path=${encodeURIComponent(path)}`,
|
||||
}
|
||||
|
||||
/** A directory in either tree: the server's mounts open like its folders. */
|
||||
const isDir = (e) => e.kind === "dir" || e.kind === "mount"
|
||||
|
||||
/** A lazily opened tree in `box`. `list(path)` -> entries; `open(entry)` for a file. */
|
||||
function tree(box, { list, open, rootPath }) {
|
||||
clear(box)
|
||||
const ul = h("ul", { class: "tree", role: "tree" })
|
||||
box.append(ul)
|
||||
const level = async (parent, path, depth) => {
|
||||
let entries
|
||||
try {
|
||||
entries = await list(path)
|
||||
} catch (err) {
|
||||
parent.append(h("li", { class: "tree__note is-error", text: why(err) }))
|
||||
return
|
||||
}
|
||||
if (!entries.length) parent.append(h("li", { class: "tree__note", text: t("Empty folder") }))
|
||||
for (const e of entries) {
|
||||
const row = h(
|
||||
"button",
|
||||
{ type: "button", class: "tree__row", title: e.path, "aria-expanded": isDir(e) ? "false" : null },
|
||||
h("span", { class: "tree__chevron" }, isDir(e) ? icon(ICONS.chevron) : null),
|
||||
icon(isDir(e) ? ICONS.folder : ICONS.file),
|
||||
h("span", { class: "tree__name", text: e.name }),
|
||||
h("span", { class: "tree__size", text: isDir(e) ? "" : bytes(e.size) }),
|
||||
)
|
||||
row.style.setProperty("--depth", String(depth))
|
||||
const li = h("li", { role: "treeitem" }, row)
|
||||
if (isDir(e)) {
|
||||
let kids = null
|
||||
row.addEventListener("click", async () => {
|
||||
const opening = row.getAttribute("aria-expanded") !== "true"
|
||||
row.setAttribute("aria-expanded", String(opening))
|
||||
if (!opening) {
|
||||
kids?.remove()
|
||||
kids = null
|
||||
return
|
||||
}
|
||||
kids = h("ul", { role: "group" })
|
||||
li.append(kids)
|
||||
await level(kids, e.path, depth + 1)
|
||||
})
|
||||
} else row.addEventListener("click", () => open(e))
|
||||
parent.append(li)
|
||||
}
|
||||
}
|
||||
return level(ul, rootPath, 0)
|
||||
}
|
||||
|
||||
// --- This computer ------------------------------------------------------------------------------
|
||||
|
||||
let local = { root: "", roots: [], fixed: false }
|
||||
|
||||
/**
|
||||
* Point this computer's pane at a folder: `fixed` (an agent chat's working directory, which is
|
||||
* what the pane shows while that chat is open) or one of the agent's folders, chosen in the pane.
|
||||
*/
|
||||
export function showLocal({ roots, root, fixed }) {
|
||||
const changed = root !== local.root || fixed !== local.fixed || roots.join("\n") !== local.roots.join("\n")
|
||||
local = { roots, root: root || (roots.includes(local.root) ? local.root : roots[0] || ""), fixed: !!fixed }
|
||||
if (changed) renderLocal()
|
||||
}
|
||||
|
||||
function renderLocal() {
|
||||
const head = clear($("local-where"))
|
||||
const box = $("local-tree")
|
||||
if (!local.roots.length) {
|
||||
clear(box).append(
|
||||
h("p", { class: "pane__note", text: t("No project folder yet: the agent can open nothing on this computer.") }),
|
||||
h("button", { type: "button", class: "btn btn--sm", text: t("Add folder…"), onclick: () => invoke("remote_add") }),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (local.fixed) {
|
||||
head.append(h("span", { class: "pane__path", title: local.root }, h("bdi", { text: local.root })))
|
||||
} else {
|
||||
const pick = h(
|
||||
"select",
|
||||
{ class: "pane__pick", "aria-label": t("Folder"), onchange: (e) => showLocal({ roots: local.roots, root: e.target.value, fixed: false }) },
|
||||
local.roots.map((r) => h("option", { value: r, text: r, selected: r === local.root })),
|
||||
)
|
||||
head.append(pick)
|
||||
}
|
||||
const root = local.root
|
||||
tree(box, {
|
||||
rootPath: "",
|
||||
list: async (path) => (await invoke("local_list", { root, path })).entries,
|
||||
open: async (e) => {
|
||||
const where = t("This computer")
|
||||
try {
|
||||
const f = await invoke("local_read", { root, path: e.path })
|
||||
openViewer({ name: f.name, path: `${f.root}/${f.path}`, where, text: f.text, withheld: f.withheld, size: f.size })
|
||||
} catch (err) {
|
||||
openViewer({ name: e.name, path: e.path, where, error: why(err) })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Read a file the agent showed in a chat on this computer: `path` from the chat's folder. */
|
||||
export async function readLocal(root, path) {
|
||||
return invoke("local_read", { root, path })
|
||||
}
|
||||
|
||||
// --- The server ---------------------------------------------------------------------------------
|
||||
|
||||
export async function readServer(path) {
|
||||
return get(SERVER_FILES.read(path))
|
||||
}
|
||||
|
||||
export function renderServer() {
|
||||
const box = $("server-tree")
|
||||
tree(box, {
|
||||
rootPath: "/",
|
||||
list: async (path) => (await get(SERVER_FILES.list(path))).entries || [],
|
||||
open: async (e) => {
|
||||
const where = t("Server")
|
||||
try {
|
||||
const f = await readServer(e.path)
|
||||
openViewer({
|
||||
name: f.name || e.name,
|
||||
path: f.path || e.path,
|
||||
where,
|
||||
text: f.text,
|
||||
// The server's own kind and language: its table and the viewer's are the same one.
|
||||
kind: ["markdown", "code", "text", "html"].includes(f.kind) ? f.kind : undefined,
|
||||
language: f.language || undefined,
|
||||
withheld: f.withheld,
|
||||
size: f.size,
|
||||
})
|
||||
} catch (err) {
|
||||
openViewer({ name: e.name, path: e.path, where, error: why(err) })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function initExplorer() {
|
||||
for (const pane of document.querySelectorAll(".pane")) {
|
||||
const toggle = pane.querySelector(".pane__toggle")
|
||||
toggle.prepend(h("span", { class: "tree__chevron" }, icon(ICONS.chevron)))
|
||||
toggle.addEventListener("click", () => {
|
||||
const open = pane.dataset.open !== "true"
|
||||
pane.dataset.open = String(open)
|
||||
toggle.setAttribute("aria-expanded", String(open))
|
||||
$("side").dataset[pane.id === "pane-local" ? "local" : "server"] = String(open)
|
||||
})
|
||||
}
|
||||
$("server-refresh").replaceChildren(icon(ICONS.refresh))
|
||||
$("server-refresh").addEventListener("click", renderServer)
|
||||
$("local-refresh").replaceChildren(icon(ICONS.refresh))
|
||||
$("local-refresh").addEventListener("click", renderLocal)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// The sidebar's chats: agent chats (the default -- the agent on this computer is what the app is
|
||||
// for) or chats on the server. Agent chats on this computer come first; those on another device
|
||||
// of the account follow under their own heading.
|
||||
|
||||
import { $, h, clear, when, append } from "./dom.js"
|
||||
import { get, why } from "./bridge.js"
|
||||
|
||||
let ctx = null // { kind(), thisDevices(), open(id), current() }
|
||||
let rows = []
|
||||
let more = false
|
||||
|
||||
/** The list again, from the server. */
|
||||
export async function refreshList() {
|
||||
const kind = ctx.kind()
|
||||
try {
|
||||
const r = await get(`/chats?kind=${kind}&limit=50`)
|
||||
if (ctx.kind() !== kind) return
|
||||
rows = r.chats || []
|
||||
more = !!r.more
|
||||
render()
|
||||
} catch (err) {
|
||||
clear($("chat-list")).append(h("li", { class: "list__note is-error", text: why(err) }))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
const last = rows[rows.length - 1]
|
||||
if (!last) return
|
||||
const kind = ctx.kind()
|
||||
const r = await get(`/chats?kind=${kind}&limit=50&before=${encodeURIComponent(last.id)}`)
|
||||
if (ctx.kind() !== kind) return
|
||||
rows = rows.concat(r.chats || [])
|
||||
more = !!r.more
|
||||
render()
|
||||
}
|
||||
|
||||
export function markCurrent() {
|
||||
const id = ctx.current()
|
||||
for (const b of $("chat-list").querySelectorAll(".chatrow")) b.setAttribute("aria-current", String(b.dataset.id === id))
|
||||
}
|
||||
|
||||
function row(c) {
|
||||
const b = h(
|
||||
"button",
|
||||
{ type: "button", class: `chatrow${c.unread ? " is-unread" : ""}`, dataset: { id: c.id }, title: c.title || "" },
|
||||
h("span", { class: `dot${c.running ? " is-busy" : c.unread ? " is-new" : ""}`, "aria-hidden": "true" }),
|
||||
h("span", { class: "row__title", text: c.title || t("New chat") }),
|
||||
h("span", { class: "row__when", text: when(c.updated_at) }),
|
||||
c.device?.directory ? h("span", { class: "row__sub", title: c.device.directory }, h("bdi", { text: c.device.directory })) : null,
|
||||
)
|
||||
b.addEventListener("click", () => ctx.open(c.id))
|
||||
return h("li", null, b)
|
||||
}
|
||||
|
||||
function render() {
|
||||
const list = clear($("chat-list"))
|
||||
const kind = ctx.kind()
|
||||
const visible = rows.filter((r) => !r.archived && !r.temporary)
|
||||
if (!visible.length) {
|
||||
list.append(
|
||||
h("li", {
|
||||
class: "list__note",
|
||||
text: kind === "agent" ? t("No agent chat yet. Start one: it works in a folder on this computer.") : t("No chat yet."),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (kind === "agent") {
|
||||
const mine = ctx.thisDevices()
|
||||
const here = visible.filter((r) => r.device && mine.includes(r.device.token_id))
|
||||
const there = visible.filter((r) => !(r.device && mine.includes(r.device.token_id)))
|
||||
if (here.length) append(list, [h("li", { class: "list__head", text: t("This computer") }), here.map(row)])
|
||||
if (there.length) append(list, [h("li", { class: "list__head", text: t("Other devices") }), there.map(row)])
|
||||
} else append(list, visible.map(row))
|
||||
if (more) list.append(h("li", null, h("button", { type: "button", class: "btn btn--sm list__more", text: t("More"), onclick: loadMore })))
|
||||
markCurrent()
|
||||
}
|
||||
|
||||
export function initList(context) {
|
||||
ctx = context
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
// The app's page: which view (the setup card, or the app), and the app itself -- the sidebar
|
||||
// (agent chats by default, chats on the server, and the two explorers), the chat, the viewer and
|
||||
// the settings. Everything it knows comes from Rust: `status` (the app, the agent, the server) and
|
||||
// `api` (the server's /api/v2, as the signed-in account).
|
||||
|
||||
import { $, h, clear, icon, ICONS } from "./dom.js"
|
||||
import { invoke, listen, get, why } from "./bridge.js"
|
||||
import { renderBar, initBar } from "./titlebar.js"
|
||||
import { renderSetup, initSetup } from "./setup.js"
|
||||
import { renderSettings, initSettings } from "./settings.js"
|
||||
import { initViewer, onViewer, closeViewer } from "./viewer.js"
|
||||
import { initExplorer, showLocal, renderServer } from "./explorer.js"
|
||||
import { initList, refreshList, markCurrent } from "./list.js"
|
||||
import { initChat, openChat, newChat, closeChat, openChatId } from "./chat.js"
|
||||
|
||||
let status = null
|
||||
let view = "" // "setup" | "app"
|
||||
let me = null
|
||||
let models = []
|
||||
let devices = [] // this account's linked devices
|
||||
let kind = "agent"
|
||||
let started = false
|
||||
let currentChat = null
|
||||
|
||||
const remembered = (() => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem("lembas.thisDevices") || "[]")
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})()
|
||||
|
||||
/**
|
||||
* The device ids that are this computer: a linked device whose host is this computer's name (the
|
||||
* CLI sends it at login). Remembered, so agent chats here are still known as here while the
|
||||
* agent is off and the device is not linked right now.
|
||||
*/
|
||||
function thisDevices() {
|
||||
let changed = false
|
||||
for (const d of devices) {
|
||||
if (status?.host && d.host === status.host && !remembered.includes(d.id)) {
|
||||
remembered.push(d.id)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
remembered.splice(0, Math.max(0, remembered.length - 8))
|
||||
try {
|
||||
localStorage.setItem("lembas.thisDevices", JSON.stringify(remembered))
|
||||
} catch {}
|
||||
}
|
||||
return remembered.slice()
|
||||
}
|
||||
|
||||
function applyTheme(name) {
|
||||
if (name !== "moria" && name !== "shire") name = "moria"
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = name
|
||||
root.dataset.base = name
|
||||
}
|
||||
|
||||
// --- Views ------------------------------------------------------------------------------------
|
||||
|
||||
function route() {
|
||||
const ready = status.checked && status.discovery && !status.refused && status.account
|
||||
const want = ready ? "app" : "setup"
|
||||
if (want !== view) {
|
||||
view = want
|
||||
$("setup").hidden = want !== "setup"
|
||||
$("shell").hidden = want !== "app"
|
||||
if (want === "app") startApp()
|
||||
else stopApp()
|
||||
}
|
||||
if (view === "setup") renderSetup(status)
|
||||
if (view === "app") {
|
||||
if (!$("settings").hidden) renderSettings(status)
|
||||
localPane()
|
||||
}
|
||||
}
|
||||
|
||||
async function startApp() {
|
||||
started = true
|
||||
try {
|
||||
me = await get("/me")
|
||||
applyTheme(me.theme)
|
||||
invoke("set_theme", { name: me.theme === "shire" ? "shire" : "moria" })
|
||||
} catch (err) {
|
||||
$("thread-note").textContent = why(err)
|
||||
$("thread-note").hidden = false
|
||||
}
|
||||
await Promise.all([loadModels(), loadDevices()])
|
||||
$("new-chat-btn").hidden = me?.permissions && me.permissions["chat.create"] === false
|
||||
setKind(kind)
|
||||
renderServer()
|
||||
invoke("remote_refresh")
|
||||
}
|
||||
|
||||
function stopApp() {
|
||||
started = false
|
||||
closeChat()
|
||||
closeViewer()
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
models = (await get("/models")).models || []
|
||||
} catch {
|
||||
models = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevices() {
|
||||
try {
|
||||
devices = (await get("/devices")).devices || []
|
||||
} catch {
|
||||
devices = []
|
||||
}
|
||||
}
|
||||
|
||||
// --- The sidebar --------------------------------------------------------------------------------
|
||||
|
||||
function setKind(k) {
|
||||
kind = k
|
||||
for (const b of document.querySelectorAll(".switch button")) b.setAttribute("aria-selected", String(b.dataset.kind === k))
|
||||
$("new-chat-btn").lastChild.textContent = k === "agent" ? t("New agent chat") : t("New chat")
|
||||
clear($("chat-list"))
|
||||
refreshList()
|
||||
// With no chat open, the main view is where a chat of this kind starts.
|
||||
if (!openChatId() && $("settings").hidden) {
|
||||
currentChat = null
|
||||
newChat(k)
|
||||
localPane()
|
||||
}
|
||||
}
|
||||
|
||||
/** This computer's pane: the open agent chat's folder when it runs here, else a folder chosen. */
|
||||
function localPane(chat) {
|
||||
const roots = status?.remote?.enabled ? status.remote.roots : []
|
||||
const c = chat || currentChat
|
||||
const here = c?.device && thisDevices().includes(c.device.token_id)
|
||||
if (here && c.device.directory) showLocal({ roots, root: c.device.directory, fixed: true })
|
||||
else showLocal({ roots, root: "", fixed: false })
|
||||
}
|
||||
|
||||
function narrow() {
|
||||
return window.matchMedia("(max-width: 48rem)").matches
|
||||
}
|
||||
|
||||
function sideOpen(on) {
|
||||
$("shell").dataset.side = String(on)
|
||||
$("side-toggle").setAttribute("aria-expanded", String(on))
|
||||
}
|
||||
|
||||
// --- Settings -----------------------------------------------------------------------------------
|
||||
|
||||
function showSettings(on) {
|
||||
$("settings").hidden = !on
|
||||
$("main").hidden = on
|
||||
if (on) {
|
||||
renderSettings(status)
|
||||
invoke("remote_refresh")
|
||||
$("settings-close").focus()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Start --------------------------------------------------------------------------------------
|
||||
|
||||
async function boot() {
|
||||
initBar()
|
||||
initViewer()
|
||||
initExplorer()
|
||||
initSetup(() => status)
|
||||
initSettings(() => status)
|
||||
initList({
|
||||
kind: () => kind,
|
||||
thisDevices,
|
||||
current: () => openChatId(),
|
||||
open: (id) => {
|
||||
showSettings(false)
|
||||
if (narrow()) sideOpen(false)
|
||||
openChat(id).then(markCurrent)
|
||||
},
|
||||
})
|
||||
initChat({
|
||||
status: () => status,
|
||||
thisDevices,
|
||||
models: () => models,
|
||||
me: () => me,
|
||||
changed: () => refreshList(),
|
||||
opened: (chat) => {
|
||||
currentChat = chat
|
||||
localPane(chat)
|
||||
markCurrent()
|
||||
},
|
||||
})
|
||||
onViewer((on) => {
|
||||
$("shell").dataset.viewer = String(on)
|
||||
})
|
||||
|
||||
for (const b of document.querySelectorAll(".switch button")) b.addEventListener("click", () => setKind(b.dataset.kind))
|
||||
$("new-chat-btn").prepend(icon(ICONS.plus))
|
||||
$("new-chat-btn").addEventListener("click", () => {
|
||||
showSettings(false)
|
||||
currentChat = null
|
||||
if (narrow()) sideOpen(false)
|
||||
newChat(kind)
|
||||
markCurrent()
|
||||
localPane()
|
||||
})
|
||||
$("settings-btn").replaceChildren(icon(ICONS.gear), h("span", { text: t("Settings") }))
|
||||
$("settings-btn").addEventListener("click", () => showSettings($("settings").hidden))
|
||||
$("settings-close").replaceChildren(icon(ICONS.close))
|
||||
$("settings-close").addEventListener("click", () => showSettings(false))
|
||||
$("side-toggle").addEventListener("click", () => sideOpen($("shell").dataset.side !== "true"))
|
||||
$("scrim").addEventListener("click", () => sideOpen(false))
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && $("shell").dataset.side === "true") sideOpen(false)
|
||||
})
|
||||
|
||||
invoke("set_words", { words: { reply: t("The reply is ready."), agent: t("The agent has finished.") } })
|
||||
listen("status", (e) => {
|
||||
const before = status
|
||||
status = e.payload
|
||||
renderBar(status)
|
||||
route()
|
||||
// The agent came up or went: which devices are linked changes with it.
|
||||
if (started && before && before.agent.state !== status.agent.state) loadDevices().then(refreshList)
|
||||
})
|
||||
listen("open-settings", () => {
|
||||
if (view === "app") showSettings(true)
|
||||
})
|
||||
status = await invoke("get_status")
|
||||
applyTheme(status.theme)
|
||||
renderBar(status)
|
||||
route()
|
||||
// The list keeps up with replies finished elsewhere (the web interface, the phone).
|
||||
setInterval(() => {
|
||||
if (view === "app" && document.visibilityState === "visible") refreshList()
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
boot()
|
||||
@@ -0,0 +1,347 @@
|
||||
// Markdown, as a model writes it, to elements -- never to an HTML string. A reply is model
|
||||
// output and untrusted: no raw HTML passes (it shows as the text it is), no image is fetched
|
||||
// (an image is its link), and a link opens in the system's browser only when it is http, https
|
||||
// or mail. What it does draw: headings, paragraphs, emphasis, strike-through, inline code, fenced
|
||||
// code, block quotes, lists (nested), tables, rules, links and line breaks. A reply still being
|
||||
// written renders the same way; an unclosed `*` is simply a star.
|
||||
|
||||
import { h } from "./dom.js"
|
||||
import { openLink } from "./bridge.js"
|
||||
|
||||
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)/
|
||||
const HEADING = /^ {0,3}(#{1,6})(?:\s+(.*?))?\s*#*\s*$/
|
||||
const RULE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/
|
||||
const QUOTE = /^ {0,3}> ?/
|
||||
const ITEM = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/
|
||||
const TABLE_RULE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/
|
||||
const SAFE_LINK = /^(https?:|mailto:)/i
|
||||
|
||||
/** The rendered fragment of `source`. */
|
||||
export function markdown(source) {
|
||||
const out = document.createDocumentFragment()
|
||||
blocks(String(source ?? "").replace(/\r\n?/g, "\n").split("\n"), out)
|
||||
return out
|
||||
}
|
||||
|
||||
function indentOf(line) {
|
||||
const m = /^\s*/.exec(line)
|
||||
return m ? m[0].replace(/\t/g, " ").length : 0
|
||||
}
|
||||
|
||||
function startsBlock(line, next) {
|
||||
return (
|
||||
FENCE.test(line) ||
|
||||
HEADING.test(line) ||
|
||||
RULE.test(line) ||
|
||||
QUOTE.test(line) ||
|
||||
ITEM.test(line) ||
|
||||
(line.includes("|") && next !== undefined && TABLE_RULE.test(next) && next.includes("-"))
|
||||
)
|
||||
}
|
||||
|
||||
function blocks(lines, parent) {
|
||||
let i = 0
|
||||
while (i < lines.length) {
|
||||
const line = lines[i]
|
||||
if (!line.trim()) {
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
const fence = FENCE.exec(line)
|
||||
if (fence) {
|
||||
const mark = fence[1]
|
||||
const body = []
|
||||
i += 1
|
||||
while (i < lines.length && !new RegExp(`^ {0,3}${mark[0]}{${mark.length},}\\s*$`).test(lines[i])) {
|
||||
body.push(lines[i])
|
||||
i += 1
|
||||
}
|
||||
i += 1 // the closing fence (or the end)
|
||||
parent.append(codeBlock(body.join("\n"), fence[2]))
|
||||
continue
|
||||
}
|
||||
const heading = HEADING.exec(line)
|
||||
if (heading) {
|
||||
parent.append(h(`h${heading[1].length}`, null, inline(heading[2] || "")))
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
if (RULE.test(line)) {
|
||||
parent.append(h("hr"))
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
if (QUOTE.test(line)) {
|
||||
const inner = []
|
||||
while (i < lines.length && lines[i].trim() && (QUOTE.test(lines[i]) || !startsBlock(lines[i], lines[i + 1]))) {
|
||||
inner.push(lines[i].replace(QUOTE, ""))
|
||||
i += 1
|
||||
}
|
||||
const quote = h("blockquote")
|
||||
blocks(inner, quote)
|
||||
parent.append(quote)
|
||||
continue
|
||||
}
|
||||
if (ITEM.test(line)) {
|
||||
i = list(lines, i, parent)
|
||||
continue
|
||||
}
|
||||
if (line.includes("|") && TABLE_RULE.test(lines[i + 1] || "") && (lines[i + 1] || "").includes("-")) {
|
||||
i = table(lines, i, parent)
|
||||
continue
|
||||
}
|
||||
const para = [line]
|
||||
i += 1
|
||||
while (i < lines.length && lines[i].trim() && !startsBlock(lines[i], lines[i + 1])) {
|
||||
para.push(lines[i])
|
||||
i += 1
|
||||
}
|
||||
parent.append(h("p", null, inline(para.join("\n"))))
|
||||
}
|
||||
}
|
||||
|
||||
function codeBlock(text, lang) {
|
||||
const code = h("code", { text })
|
||||
const copy = h("button", {
|
||||
type: "button",
|
||||
class: "md-copy",
|
||||
text: t("Copy"),
|
||||
onclick: () => navigator.clipboard?.writeText(text).catch(() => {}),
|
||||
})
|
||||
return h("div", { class: "md-code" }, lang ? h("span", { class: "md-lang", text: lang }) : null, copy, h("pre", null, code))
|
||||
}
|
||||
|
||||
function list(lines, start, parent) {
|
||||
const first = ITEM.exec(lines[start])
|
||||
const base = indentOf(first[1])
|
||||
const ordered = /\d/.test(first[2])
|
||||
const el = h(ordered ? "ol" : "ul")
|
||||
if (ordered) {
|
||||
const n = parseInt(first[2], 10)
|
||||
if (n !== 1) el.setAttribute("start", String(n))
|
||||
}
|
||||
let i = start
|
||||
while (i < lines.length) {
|
||||
const m = ITEM.exec(lines[i])
|
||||
if (!m || indentOf(m[1]) !== base || /\d/.test(m[2]) !== ordered) break
|
||||
const content = [m[3]]
|
||||
const width = indentOf(m[1]) + m[2].length + 1
|
||||
i += 1
|
||||
while (i < lines.length) {
|
||||
const l = lines[i]
|
||||
if (!l.trim()) {
|
||||
// A blank line inside an item, when the item goes on indented after it.
|
||||
if (i + 1 < lines.length && indentOf(lines[i + 1]) > base && lines[i + 1].trim()) {
|
||||
content.push("")
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
const sub = ITEM.exec(l)
|
||||
if (sub && indentOf(sub[1]) <= base) break
|
||||
if (indentOf(l) <= base && startsBlock(l, lines[i + 1])) break
|
||||
content.push(l.slice(Math.min(width, indentOf(l))))
|
||||
i += 1
|
||||
}
|
||||
const li = h("li")
|
||||
const task = /^\[([ xX])\]\s+/.exec(content[0])
|
||||
if (task) {
|
||||
content[0] = content[0].slice(task[0].length)
|
||||
li.classList.add("md-task")
|
||||
li.append(h("input", { type: "checkbox", disabled: true, checked: task[1] !== " " }))
|
||||
}
|
||||
// A tight item is its text; one with blocks in it is blocks.
|
||||
if (content.length === 1 || !content.slice(1).some((c) => startsBlock(c) || !c.trim())) {
|
||||
li.append(...inline(content.join("\n")))
|
||||
} else blocks(content, li)
|
||||
el.append(li)
|
||||
while (i < lines.length && !lines[i].trim()) {
|
||||
const next = ITEM.exec(lines[i + 1] || "")
|
||||
if (next && indentOf(next[1]) === base) i += 1
|
||||
else break
|
||||
}
|
||||
}
|
||||
parent.append(el)
|
||||
return i
|
||||
}
|
||||
|
||||
function cells(line) {
|
||||
let s = line.trim()
|
||||
if (s.startsWith("|")) s = s.slice(1)
|
||||
if (s.endsWith("|") && !s.endsWith("\\|")) s = s.slice(0, -1)
|
||||
return s.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"))
|
||||
}
|
||||
|
||||
function table(lines, start, parent) {
|
||||
const head = cells(lines[start])
|
||||
const aligns = cells(lines[start + 1]).map((c) =>
|
||||
c.startsWith(":") && c.endsWith(":") ? "center" : c.endsWith(":") ? "right" : c.startsWith(":") ? "left" : "",
|
||||
)
|
||||
let i = start + 2
|
||||
const rows = []
|
||||
while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
|
||||
rows.push(cells(lines[i]))
|
||||
i += 1
|
||||
}
|
||||
const cell = (tag, text, n) => {
|
||||
const c = h(tag, null, inline(text ?? ""))
|
||||
if (aligns[n]) c.dataset.align = aligns[n]
|
||||
return c
|
||||
}
|
||||
const tbl = h(
|
||||
"table",
|
||||
null,
|
||||
h("thead", null, h("tr", null, head.map((c, n) => cell("th", c, n)))),
|
||||
h("tbody", null, rows.map((r) => h("tr", null, head.map((_, n) => cell("td", r[n], n))))),
|
||||
)
|
||||
// A wide table scrolls by itself; the page does not.
|
||||
parent.append(h("div", { class: "md-table" }, tbl))
|
||||
return i
|
||||
}
|
||||
|
||||
function link(label, url) {
|
||||
const safe = SAFE_LINK.test(url)
|
||||
const a = h("a", { href: safe ? url : null, title: url, class: safe ? null : "md-unsafe" }, label)
|
||||
if (safe) {
|
||||
a.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
openLink(url)
|
||||
})
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
/** The closing `mark` for an opening at `from`, or -1. */
|
||||
function closing(text, mark, from) {
|
||||
let at = text.indexOf(mark, from)
|
||||
while (at !== -1) {
|
||||
const before = text[at - 1]
|
||||
if (at > from && before !== " " && before !== "\\") {
|
||||
if (mark !== "_" || !/\w/.test(text[at + 1] || "")) return at
|
||||
}
|
||||
at = text.indexOf(mark, at + 1)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/** Inline markdown to nodes. */
|
||||
export function inline(text) {
|
||||
const out = []
|
||||
let plain = ""
|
||||
const flush = () => {
|
||||
if (plain) out.push(document.createTextNode(plain))
|
||||
plain = ""
|
||||
}
|
||||
let i = 0
|
||||
while (i < text.length) {
|
||||
const c = text[i]
|
||||
const rest = text.slice(i)
|
||||
if (c === "\\" && i + 1 < text.length) {
|
||||
if (text[i + 1] === "\n") {
|
||||
flush()
|
||||
out.push(h("br"))
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (/[\\`*_{}[\]()#+\-.!|~<>]/.test(text[i + 1])) {
|
||||
plain += text[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (c === "`") {
|
||||
const run = /^`+/.exec(rest)[0]
|
||||
const end = text.indexOf(run, i + run.length)
|
||||
if (end !== -1) {
|
||||
flush()
|
||||
out.push(h("code", { text: text.slice(i + run.length, end).replace(/^ (.+) $/, "$1") }))
|
||||
i = end + run.length
|
||||
continue
|
||||
}
|
||||
plain += run
|
||||
i += run.length
|
||||
continue
|
||||
}
|
||||
const img = /^!\[([^\]]*)\]\(\s*<?([^\s)>]+)>?(?:\s+"[^"]*")?\s*\)/.exec(rest)
|
||||
if (img) {
|
||||
flush()
|
||||
out.push(link(img[1] || img[2], img[2]))
|
||||
i += img[0].length
|
||||
continue
|
||||
}
|
||||
if (c === "[") {
|
||||
const m = /^\[((?:[^[\]\\]|\\.|\[[^\]]*\])*)\]\(\s*<?([^\s)>]+)>?(?:\s+"[^"]*")?\s*\)/.exec(rest)
|
||||
if (m) {
|
||||
flush()
|
||||
out.push(link(inline(m[1]), m[2]))
|
||||
i += m[0].length
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (c === "<") {
|
||||
const m = /^<((?:https?:\/\/|mailto:)[^\s>]+)>/i.exec(rest)
|
||||
if (m) {
|
||||
flush()
|
||||
out.push(link(m[1], m[1]))
|
||||
i += m[0].length
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((c === "h" || c === "H") && !/\w/.test(text[i - 1] || "")) {
|
||||
const m = /^https?:\/\/[^\s<]+/i.exec(rest)
|
||||
if (m) {
|
||||
let url = m[0]
|
||||
// Trailing punctuation belongs to the sentence, and an unmatched ")" to a parenthesis.
|
||||
while (/[.,;:!?'"]$/.test(url) || (url.endsWith(")") && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length)) {
|
||||
url = url.slice(0, -1)
|
||||
}
|
||||
flush()
|
||||
out.push(link(url, url))
|
||||
i += url.length
|
||||
continue
|
||||
}
|
||||
}
|
||||
const strong = rest.startsWith("**") ? "**" : rest.startsWith("__") && !/\w/.test(text[i - 1] || "") ? "__" : ""
|
||||
if (strong && text[i + 2] && text[i + 2] !== " ") {
|
||||
const end = closing(text, strong, i + 2)
|
||||
if (end !== -1) {
|
||||
flush()
|
||||
out.push(h("strong", null, inline(text.slice(i + 2, end))))
|
||||
i = end + 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (rest.startsWith("~~") && text[i + 2] && text[i + 2] !== " ") {
|
||||
const end = closing(text, "~~", i + 2)
|
||||
if (end !== -1) {
|
||||
flush()
|
||||
out.push(h("del", null, inline(text.slice(i + 2, end))))
|
||||
i = end + 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ((c === "*" || (c === "_" && !/\w/.test(text[i - 1] || ""))) && text[i + 1] && !/\s/.test(text[i + 1]) && text[i + 1] !== c) {
|
||||
const end = closing(text, c, i + 1)
|
||||
if (end !== -1) {
|
||||
flush()
|
||||
out.push(h("em", null, inline(text.slice(i + 1, end))))
|
||||
i = end + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (c === "\n") {
|
||||
if (plain.endsWith(" ")) {
|
||||
plain = plain.replace(/ +$/, "")
|
||||
flush()
|
||||
out.push(h("br"))
|
||||
} else plain += " "
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
plain += c
|
||||
i += 1
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Settings: the app's, not the server's (no admin page here, ever -- an admin sees what everyone
|
||||
// sees). The server and the app's sign-in, the agent and the folders it may work in, the
|
||||
// notifications, and this release.
|
||||
|
||||
import { $, h, clear } from "./dom.js"
|
||||
import { invoke, listen, why } from "./bridge.js"
|
||||
|
||||
const AGENT = {
|
||||
off: () => [t("The agent is off."), ""],
|
||||
not_linked: () => [t("This computer is not linked to the server yet."), ""],
|
||||
starting: () => [t("The agent is starting…"), ""],
|
||||
running: () => [t("The agent is running: agent chats can use this computer."), "is-ok"],
|
||||
elsewhere: (pid) => [t("A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.", { pid }), "is-ok"],
|
||||
failed: (why) => [t("The agent stopped: %(why)s. Trying again shortly.", { why }), "is-error"],
|
||||
}
|
||||
|
||||
let folderError = ""
|
||||
let linking = null // { user_code, uri } while the computer is being linked
|
||||
let linkError = ""
|
||||
|
||||
export function agentWords(s) {
|
||||
const noFolders = s.remote && !(s.remote.enabled && s.remote.roots.length)
|
||||
if (s.agent.state === "running" && noFolders) return [t("The agent is connected, but may open no folder yet: add one below."), "is-error"]
|
||||
return (AGENT[s.agent.state] ?? AGENT.off)(s.agent.detail)
|
||||
}
|
||||
|
||||
function section(id, title, ...body) {
|
||||
return h("section", { class: "set", "aria-labelledby": id }, h("h2", { id, text: title }), body)
|
||||
}
|
||||
|
||||
export function renderSettings(s) {
|
||||
const box = clear($("settings-body"))
|
||||
// The server and the account.
|
||||
const who = s.account ? [s.account.name, s.account.email].filter(Boolean).join(" · ") : ""
|
||||
box.append(
|
||||
section(
|
||||
"set-server",
|
||||
t("Server"),
|
||||
h("p", null, h("bdi", { text: s.server || "" }), s.discovery ? h("span", { class: "muted", text: ` · LLeMbas ${s.discovery.version}` }) : null),
|
||||
who ? h("p", { class: "muted", text: t("Signed in as %(who)s.", { who }) }) : null,
|
||||
h("p", { class: "muted", text: t("Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.") }),
|
||||
h("div", { class: "row--buttons" }, h("button", { type: "button", class: "btn", text: t("Sign out"), onclick: () => invoke("sign_out") })),
|
||||
),
|
||||
)
|
||||
|
||||
// The agent.
|
||||
const [text, cls] = agentWords(s)
|
||||
const agent = section(
|
||||
"set-agent",
|
||||
t("Agent"),
|
||||
h("p", { class: "muted", text: t("The agent lets agent chats on your server work in this computer's projects. It is the LLeMbas CLI, built in, running in the background while the app runs.") }),
|
||||
h("p", { class: `status ${cls}`, "aria-live": "polite", text }),
|
||||
)
|
||||
if (linking) {
|
||||
agent.append(h("p", null, t("Approve this code in the page that opened in your browser:")), h("p", { class: "code", text: linking.user_code }))
|
||||
}
|
||||
if (linkError) agent.append(h("p", { class: "status is-error", text: linkError }))
|
||||
agent.append(
|
||||
h(
|
||||
"div",
|
||||
{ class: "row--buttons" },
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn",
|
||||
text: s.linked ? t("Link again") : t("Link this computer"),
|
||||
onclick: () => {
|
||||
linkError = ""
|
||||
invoke("link_computer").catch((err) => {
|
||||
linkError = why(err)
|
||||
renderSettings(s)
|
||||
})
|
||||
},
|
||||
}),
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn",
|
||||
text: s.agent_wanted ? t("Stop the agent") : t("Start the agent"),
|
||||
disabled: !s.linked && !s.agent_wanted,
|
||||
onclick: () => invoke("set_agent", { on: !s.agent_wanted }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
box.append(agent)
|
||||
|
||||
// The folders.
|
||||
const r = s.remote
|
||||
if (s.linked && r) {
|
||||
const list = h("ul", { class: "folders" })
|
||||
if (!r.enabled || !r.roots.length) list.append(h("li", { class: "empty", text: t("No folder yet: the agent can open nothing. Add the folder of a project.") }))
|
||||
for (const dir of r.enabled ? r.roots : []) {
|
||||
list.append(
|
||||
h(
|
||||
"li",
|
||||
null,
|
||||
h("span", { class: "path", title: dir }, h("bdi", { text: dir })),
|
||||
h("button", { type: "button", class: "btn", text: t("Remove"), "aria-label": t("Remove %(dir)s", { dir }), onclick: () => invoke("remote_remove", { dir }) }),
|
||||
),
|
||||
)
|
||||
}
|
||||
const mode = h(
|
||||
"select",
|
||||
{ id: "max-mode", onchange: (e) => invoke("remote_mode", { mode: e.target.value }) },
|
||||
[
|
||||
["manual", t("Manual — asks first")],
|
||||
["edit", t("Edit — asks before commands")],
|
||||
["auto", t("Auto — asks nothing")],
|
||||
["plan", t("Plan — changes nothing")],
|
||||
].map(([v, label]) => h("option", { value: v, text: label, selected: r.max_mode === v })),
|
||||
)
|
||||
const terminal = h("input", { type: "checkbox", id: "terminal", checked: r.terminal, onchange: (e) => invoke("remote_terminal", { on: e.target.checked }) })
|
||||
box.append(
|
||||
section(
|
||||
"set-folders",
|
||||
t("Project folders"),
|
||||
h("p", { class: "muted", text: t("Agent chats can work in these folders and anything inside them — nowhere else on this computer.") }),
|
||||
list,
|
||||
folderError ? h("p", { class: "status is-error", text: folderError }) : null,
|
||||
h("div", { class: "row--buttons" }, h("button", { type: "button", class: "btn btn--primary", text: t("Add folder…"), onclick: () => invoke("remote_add") })),
|
||||
h(
|
||||
"div",
|
||||
{ class: "fields" },
|
||||
h("div", { class: "field" }, h("label", { for: "max-mode", text: t("The most an agent chat may do here") }), mode, h("p", { class: "hint", text: t("A chat asking for more is lowered to this.") })),
|
||||
h(
|
||||
"div",
|
||||
{ class: "field" },
|
||||
h("span", { class: "label", text: t("Terminal") }),
|
||||
h("label", { class: "check" }, terminal, h("span", { text: t("Let the server open a terminal here") })),
|
||||
h("p", { class: "hint", text: t("A shell as you, in one of these folders.") }),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Notifications.
|
||||
box.append(
|
||||
section(
|
||||
"set-notify",
|
||||
t("Notifications"),
|
||||
h(
|
||||
"label",
|
||||
{ class: "check" },
|
||||
h("input", { type: "checkbox", checked: s.notify, onchange: (e) => invoke("set_notify", { on: e.target.checked }) }),
|
||||
h("span", { text: t("Tell me when a reply or an agent's work is finished while I am elsewhere") }),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// This release.
|
||||
box.append(
|
||||
section(
|
||||
"set-about",
|
||||
t("About"),
|
||||
h("p", null, `LLeMbas Desktop ${s.version}`),
|
||||
h(
|
||||
"div",
|
||||
{ class: "row--buttons" },
|
||||
h("button", { type: "button", class: "btn", text: s.update ? t("Install LLeMbas %(version)s…", { version: s.update }) : t("Check for updates…"), onclick: () => invoke("update_app") }),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function initSettings(current) {
|
||||
listen("remote-error", (e) => {
|
||||
folderError = String(e.payload)
|
||||
renderSettings(current())
|
||||
})
|
||||
listen("status", () => {
|
||||
folderError = ""
|
||||
})
|
||||
listen("linking", (e) => {
|
||||
const p = e.payload
|
||||
linking = p.state === "code" ? { user_code: p.user_code, uri: p.uri } : null
|
||||
linkError = p.state === "failed" ? p.message : ""
|
||||
renderSettings(current())
|
||||
})
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Before the app: which server, and the app's sign-in to it. A server older than LLeMbas 2.0 is
|
||||
// refused here, with the reason Rust gave (src-tauri/src/server.rs, `check`). Signing in is the
|
||||
// server's device login: the approval page opens in the system's browser; this page shows the
|
||||
// code to compare and waits. Where the server offers it (`one_approval`) and this computer is not
|
||||
// linked yet, the same approval links it for the agent: there is no second one.
|
||||
|
||||
import { $, h, clear } from "./dom.js"
|
||||
import { invoke, listen, why } from "./bridge.js"
|
||||
|
||||
let code = null // the code being approved, while signing in
|
||||
let failed = ""
|
||||
|
||||
export function renderSetup(s) {
|
||||
const box = clear($("setup-body"))
|
||||
$("setup-version").textContent = s.version
|
||||
const server = h("section", { "aria-labelledby": "setup-server-h" }, h("h2", { id: "setup-server-h", text: t("Server") }))
|
||||
const address = h("input", {
|
||||
id: "address",
|
||||
name: "address",
|
||||
type: "text",
|
||||
inputmode: "url",
|
||||
autocomplete: "url",
|
||||
placeholder: "https://ai.example",
|
||||
spellcheck: "false",
|
||||
value: s.server || "",
|
||||
})
|
||||
const hint = h("p", { class: "hint", id: "server-hint", "aria-live": "polite" })
|
||||
if (!s.checked) hint.textContent = t("Looking for a LLeMbas server there…")
|
||||
else if (s.refused) {
|
||||
hint.textContent = s.refused
|
||||
hint.classList.add("is-error")
|
||||
} else if (s.discovery) hint.textContent = t("%(name)s, LLeMbas %(version)s.", { name: s.discovery.name, version: s.discovery.version })
|
||||
else hint.textContent = t("LLeMbas Desktop needs a LLeMbas 2.0 server or later.")
|
||||
const connect = h("button", { type: "submit", class: "btn btn--primary", text: t("Connect") })
|
||||
const form = h(
|
||||
"form",
|
||||
{ class: "field", novalidate: true },
|
||||
h("label", { for: "address", text: t("Address of your LLeMbas server") }),
|
||||
h("div", { class: "row" }, address, connect),
|
||||
hint,
|
||||
)
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault()
|
||||
connect.disabled = true
|
||||
hint.className = "hint"
|
||||
hint.textContent = t("Looking for a LLeMbas server there…")
|
||||
try {
|
||||
await invoke("set_server", { address: address.value })
|
||||
} catch (err) {
|
||||
hint.className = "hint is-error"
|
||||
hint.textContent = why(err)
|
||||
} finally {
|
||||
connect.disabled = false
|
||||
}
|
||||
})
|
||||
server.append(form)
|
||||
if (s.refused && s.server) {
|
||||
server.append(h("div", { class: "row--buttons" }, h("button", { type: "button", class: "btn", text: t("Try again"), onclick: () => invoke("recheck_server") })))
|
||||
}
|
||||
box.append(server)
|
||||
|
||||
if (s.discovery && !s.account) {
|
||||
const sign = h("section", { "aria-labelledby": "setup-sign-h" }, h("h2", { id: "setup-sign-h", text: t("Sign in") }))
|
||||
const both = s.discovery.one_approval && !s.linked
|
||||
sign.append(h("p", { class: "muted", text: both ? t("The app signs in to your server once, in your browser, as any device does. It may then read and write your chats, as the web interface does, and the same approval links this computer for the agent.") : t("The app signs in to your server once, in your browser, as any device does. It may then read and write your chats, as the web interface does.") }))
|
||||
if (s.signing_in && code) {
|
||||
sign.append(
|
||||
h("p", null, t("Approve this code in the page that opened in your browser:")),
|
||||
h("p", { class: "code", text: code.user_code }),
|
||||
h(
|
||||
"div",
|
||||
{ class: "row--buttons" },
|
||||
h("button", { type: "button", class: "btn", text: t("Open the page again"), onclick: () => invoke("open_external", { url: code.verification_uri_complete }) }),
|
||||
h("button", { type: "button", class: "btn", text: t("Cancel"), onclick: () => invoke("sign_in_cancel") }),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
sign.append(
|
||||
h(
|
||||
"div",
|
||||
{ class: "row--buttons" },
|
||||
h("button", {
|
||||
type: "button",
|
||||
class: "btn btn--primary",
|
||||
text: s.signing_in ? t("Waiting for the browser…") : t("Sign in"),
|
||||
disabled: s.signing_in,
|
||||
onclick: () => {
|
||||
failed = ""
|
||||
invoke("sign_in").catch((err) => {
|
||||
failed = why(err)
|
||||
renderSetup(s)
|
||||
})
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (failed) sign.append(h("p", { class: "status is-error", text: failed }))
|
||||
box.append(sign)
|
||||
}
|
||||
}
|
||||
|
||||
export function initSetup(current) {
|
||||
listen("signin", (e) => {
|
||||
const p = e.payload
|
||||
if (p.state === "code") code = p.code
|
||||
if (p.state === "done") code = null
|
||||
if (p.state === "failed") {
|
||||
code = null
|
||||
failed = p.message
|
||||
}
|
||||
renderSetup(current())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// The title bar: the app's own, in the page (the window has no system decoration). Moving and
|
||||
// double-click maximising come from `data-tauri-drag-region`; the edges resize the window (Tauri
|
||||
// does that for an undecorated one); the menu and the agent pill open the app's native menu.
|
||||
|
||||
import { $, icon, ICONS } from "./dom.js"
|
||||
import { invoke, listen, win } from "./bridge.js"
|
||||
|
||||
const WORDS = {
|
||||
off: () => t("agent off"),
|
||||
not_linked: () => t("not linked"),
|
||||
starting: () => t("agent starting…"),
|
||||
running: () => t("agent running"),
|
||||
elsewhere: () => t("agent running (service)"),
|
||||
failed: () => t("agent stopped"),
|
||||
no_folders: () => t("agent: no folders"),
|
||||
}
|
||||
|
||||
export function renderBar(s) {
|
||||
let host = ""
|
||||
try {
|
||||
host = s.server ? new URL(s.server).host : ""
|
||||
} catch {}
|
||||
$("host").textContent = host
|
||||
$("host").title = s.server || ""
|
||||
// Connected but allowed into no folder: a green light would say it works when it cannot.
|
||||
const noFolders = s.remote && !(s.remote.enabled && s.remote.roots.length)
|
||||
const state = s.agent.state === "running" && noFolders ? "no_folders" : s.agent.state
|
||||
$("agent").dataset.state = state
|
||||
$("agent-text").textContent = WORDS[state] ? WORDS[state]() : state
|
||||
$("agent").title = state === "failed" ? t("Agent stopped: %(why)s", { why: s.agent.detail }) : t("Agent")
|
||||
$("update").hidden = !s.update
|
||||
$("update").textContent = s.update ? t("update %(version)s", { version: s.update }) : t("update")
|
||||
$("update").title = s.update ? t("LLeMbas %(version)s is available (this is %(current)s)", { version: s.update, current: s.version }) : ""
|
||||
}
|
||||
|
||||
function maximized(on) {
|
||||
const b = $("maximize")
|
||||
b.replaceChildren(icon(on ? ICONS.restore : ICONS.max))
|
||||
b.setAttribute("aria-label", on ? t("Restore") : t("Maximise"))
|
||||
b.title = on ? t("Restore") : t("Maximise")
|
||||
}
|
||||
|
||||
// The menu opens under the button that was clicked: its bottom-left corner, in page pixels.
|
||||
function menuAt(button) {
|
||||
const r = button.getBoundingClientRect()
|
||||
return invoke("titlebar_menu", { x: r.left, y: r.bottom })
|
||||
}
|
||||
|
||||
export function initBar() {
|
||||
$("minimize").replaceChildren(icon(ICONS.min))
|
||||
$("close").replaceChildren(icon(ICONS.close))
|
||||
$("menu").replaceChildren(icon(ICONS.menu))
|
||||
$("side-toggle").replaceChildren(icon(ICONS.side))
|
||||
$("minimize").addEventListener("click", () => win.minimize())
|
||||
$("maximize").addEventListener("click", () => win.toggleMaximize())
|
||||
$("close").addEventListener("click", () => win.close())
|
||||
$("update").addEventListener("click", () => invoke("update_app"))
|
||||
$("menu").addEventListener("click", (e) => menuAt(e.currentTarget))
|
||||
$("agent").addEventListener("click", (e) => menuAt(e.currentTarget))
|
||||
listen("maximized", (e) => maximized(!!e.payload))
|
||||
win.isMaximized().then(maximized).catch(() => maximized(false))
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// The viewer beside the chat: a file from this computer, from the server, or one the agent showed
|
||||
// (`show`). Markdown is drawn (`markdown.js`), code and text are CodeMirror, read-only, and HTML
|
||||
// is a sealed frame (src-tauri/src/sealed.rs): its own scheme, no bridge, no network, no way
|
||||
// out. What a file says is never part of the app's page.
|
||||
|
||||
import { $, h, clear, icon, ICONS, bytes } from "./dom.js"
|
||||
import { invoke } from "./bridge.js"
|
||||
import { markdown } from "./markdown.js"
|
||||
import { codeView } from "./code.js"
|
||||
|
||||
// The viewer's kind by a file's name: the CLI's and the server's `showKind`, the same table.
|
||||
const LANGUAGES = {
|
||||
ts: "typescript", tsx: "tsx", js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "jsx", json: "json", py: "python", rs: "rust", go: "go",
|
||||
rb: "ruby", php: "php", java: "java", kt: "kotlin", swift: "swift", c: "c", h: "c", cc: "cpp", cpp: "cpp", hpp: "cpp", cs: "csharp",
|
||||
sh: "bash", bash: "bash", zsh: "bash", fish: "fish", css: "css", scss: "scss", sql: "sql", yaml: "yaml", yml: "yaml", toml: "toml",
|
||||
xml: "xml", svg: "xml", lua: "lua", zig: "zig", dart: "dart", vue: "vue", svelte: "svelte",
|
||||
}
|
||||
|
||||
export function showKind(path) {
|
||||
const name = String(path).split("/").pop() || ""
|
||||
const ext = name.includes(".") ? name.split(".").pop().toLowerCase() : ""
|
||||
if (ext === "md" || ext === "markdown") return { kind: "markdown", language: "markdown" }
|
||||
if (ext === "html" || ext === "htm") return { kind: "html", language: "html" }
|
||||
const language = LANGUAGES[ext] || (name === "Dockerfile" ? "dockerfile" : name === "Makefile" ? "makefile" : "")
|
||||
return language ? { kind: "code", language } : { kind: "text", language: "" }
|
||||
}
|
||||
|
||||
let current = null // { editor, sealed }
|
||||
let onChange = () => {}
|
||||
|
||||
/** Called when the viewer opens or closes (the layout makes room for it). */
|
||||
export function onViewer(fn) {
|
||||
onChange = fn
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
if (!current) return
|
||||
current.editor?.destroy()
|
||||
if (current.sealed) invoke("sealed_close", { url: current.sealed })
|
||||
current = null
|
||||
}
|
||||
|
||||
export function closeViewer() {
|
||||
teardown()
|
||||
$("viewer").hidden = true
|
||||
clear($("viewer-body"))
|
||||
onChange(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a file has no text, in words: this computer's `withheld` is "binary" | "too_big"; the
|
||||
* server's is { reason: "not_text" | "too_large", message } with the message in the person's
|
||||
* language already.
|
||||
*/
|
||||
function withheldWords(withheld, size) {
|
||||
if (typeof withheld === "object" && withheld.message) return String(withheld.message)
|
||||
const reason = typeof withheld === "object" ? withheld.reason : withheld
|
||||
return reason === "too_big" || reason === "too_large"
|
||||
? t("This file is too big to show here (%(size)s).", { size: bytes(size) })
|
||||
: t("This is not a text file; it cannot be shown here.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a file. `file`: { name, path, where (words for its place), text, kind?, language?, line?,
|
||||
* withheld? (see withheldWords), size?, error? }.
|
||||
*/
|
||||
export async function openViewer(file) {
|
||||
teardown()
|
||||
const kind = file.kind || showKind(file.path || file.name).kind
|
||||
const lang = file.language ?? showKind(file.path || file.name).language
|
||||
$("viewer").hidden = false
|
||||
onChange(true)
|
||||
$("viewer-name").textContent = file.name || file.path || ""
|
||||
$("viewer-name").title = file.path || ""
|
||||
$("viewer-where").textContent = file.where || ""
|
||||
const body = clear($("viewer-body"))
|
||||
const modes = clear($("viewer-modes"))
|
||||
current = {}
|
||||
if (file.error) {
|
||||
body.append(h("p", { class: "viewer__note is-error", text: file.error }))
|
||||
return
|
||||
}
|
||||
if (file.withheld) {
|
||||
body.append(h("p", { class: "viewer__note", text: withheldWords(file.withheld, file.size) }))
|
||||
return
|
||||
}
|
||||
const text = file.text ?? ""
|
||||
// Markdown and HTML can be seen drawn or as their source.
|
||||
const drawn = kind === "markdown" || kind === "html"
|
||||
const show = async (asSource) => {
|
||||
teardown()
|
||||
current = {}
|
||||
clear(body)
|
||||
for (const b of modes.querySelectorAll("button")) b.setAttribute("aria-pressed", String(b.dataset.source === String(asSource)))
|
||||
if (asSource || !drawn) {
|
||||
const holder = h("div", { class: "viewer__code" })
|
||||
body.append(holder)
|
||||
current.editor = await codeView(holder, text, { lang, line: file.line || 0 })
|
||||
} else if (kind === "markdown") {
|
||||
body.append(h("div", { class: "md viewer__md" }, markdown(text)))
|
||||
} else {
|
||||
const url = await invoke("sealed_open", { html: text })
|
||||
current.sealed = url
|
||||
// No allow-same-origin, no allow-top-navigation: an opaque page that can run its own
|
||||
// scripts and nothing else. The address's own policy says the same again.
|
||||
body.append(h("iframe", { class: "viewer__frame", src: url, sandbox: "allow-scripts", referrerpolicy: "no-referrer", title: file.name || "" }))
|
||||
}
|
||||
}
|
||||
if (drawn) {
|
||||
modes.append(
|
||||
h("button", { type: "button", class: "seg", dataset: { source: "false" }, text: kind === "html" ? t("Page") : t("Preview"), onclick: () => show(false) }),
|
||||
h("button", { type: "button", class: "seg", dataset: { source: "true" }, text: t("Source"), onclick: () => show(true) }),
|
||||
)
|
||||
}
|
||||
await show(false)
|
||||
}
|
||||
|
||||
export function initViewer() {
|
||||
$("viewer-close").replaceChildren(icon(ICONS.close))
|
||||
$("viewer-close").addEventListener("click", closeViewer)
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && !$("viewer").hidden && !e.defaultPrevented) closeViewer()
|
||||
})
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/* The title bar: one row, fixed height, every slot a fixed or shared track so nothing in it
|
||||
moves when the host name or the agent's state changes length. */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: var(--titlebar-h);
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: grid;
|
||||
/* leaf · name · host (takes the rest, and is the drag area) · agent · menu · sep · 3 controls */
|
||||
grid-template-columns: var(--titlebar-h) auto minmax(0, 1fr) auto var(--titlebar-h) 1px repeat(3, 2.875rem);
|
||||
align-items: center;
|
||||
height: var(--titlebar-h);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font: 600 var(--text-sm) / 1 var(--font-ui);
|
||||
}
|
||||
|
||||
.bar__leaf { justify-self: center; width: 18px; height: 18px; image-rendering: pixelated; }
|
||||
.bar__name b { color: var(--leaf); font-weight: 700; }
|
||||
.bar__host {
|
||||
padding: 0 var(--sp-3);
|
||||
color: var(--ink-muted);
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
height: var(--titlebar-h);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: var(--ink-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { color: var(--ink); background: var(--surface-hover); }
|
||||
button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
|
||||
.bar__agent {
|
||||
display: grid;
|
||||
grid-template-columns: 0.5rem auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
padding: 0 var(--sp-3);
|
||||
font-weight: 400;
|
||||
}
|
||||
.dot { width: 0.5rem; height: 0.5rem; background: var(--ink-muted); }
|
||||
.bar__agent[data-state="running"] .dot,
|
||||
.bar__agent[data-state="elsewhere"] .dot { background: var(--leaf); }
|
||||
.bar__agent[data-state="starting"] .dot { background: var(--accent); }
|
||||
.bar__agent[data-state="failed"] .dot,
|
||||
.bar__agent[data-state="not_linked"] .dot { background: var(--warning); }
|
||||
|
||||
.bar__sep { width: 1px; height: 1.25rem; background: var(--border); }
|
||||
|
||||
.bar__btn { display: grid; place-items: center; }
|
||||
.bar__btn svg { width: 16px; height: 16px; }
|
||||
.bar__btn--close:hover { color: #fff; background: var(--danger); }
|
||||
|
||||
#maximize .i-restore { display: none; }
|
||||
#maximize[data-maximized] .i-max { display: none; }
|
||||
#maximize[data-maximized] .i-restore { display: block; }
|
||||
|
||||
/* Narrow windows: the host goes first, then the agent's words (its dot stays). */
|
||||
@media (max-width: 34rem) {
|
||||
.bar__host { visibility: hidden; }
|
||||
#agent-text { display: none; }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>LLeMbas</title>
|
||||
<link rel="stylesheet" href="tokens.css">
|
||||
<link rel="stylesheet" href="titlebar.css">
|
||||
<script src="titlebar.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="bar" data-tauri-drag-region>
|
||||
<img class="bar__leaf" src="logo-mark.svg" alt="" width="18" height="18" data-tauri-drag-region>
|
||||
<span class="bar__name" data-tauri-drag-region><b>LL</b>e<b>M</b>bas</span>
|
||||
<span class="bar__host" id="host" data-tauri-drag-region></span>
|
||||
|
||||
<button class="bar__agent" id="agent" type="button" title="Agent">
|
||||
<span class="dot" aria-hidden="true"></span><span id="agent-text">agent off</span>
|
||||
</button>
|
||||
<button class="bar__btn" id="menu" type="button" aria-label="Menu" title="Menu">
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><g fill="currentColor"><rect x="3" y="4" width="10" height="1.5"/><rect x="3" y="7.25" width="10" height="1.5"/><rect x="3" y="10.5" width="10" height="1.5"/></g></svg>
|
||||
</button>
|
||||
<span class="bar__sep" aria-hidden="true"></span>
|
||||
<button class="bar__btn" id="minimize" type="button" aria-label="Minimise" title="Minimise">
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="10" width="8" height="1.5" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="bar__btn" id="maximize" type="button" aria-label="Maximise" title="Maximise">
|
||||
<svg class="i-max" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.75 4.75h6.5v6.5h-6.5z" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
<svg class="i-restore" viewBox="0 0 16 16" aria-hidden="true"><path d="M3.75 6.25h6v6h-6z M6.25 6.25v-2.5h6v6h-2.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
</button>
|
||||
<button class="bar__btn bar__btn--close" id="close" type="button" aria-label="Close" title="Close (LLeMbas stays in the tray)">
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,50 +0,0 @@
|
||||
// The title bar. Moving and double-click maximising come from `data-tauri-drag-region`; the
|
||||
// buttons call the window directly; the menu and the agent pill open the app's native menu (the
|
||||
// tray's items), which can reach past this 36-pixel-high page.
|
||||
"use strict"
|
||||
|
||||
const { invoke } = window.__TAURI__.core
|
||||
const { listen } = window.__TAURI__.event
|
||||
const win = window.__TAURI__.window.getCurrentWindow()
|
||||
|
||||
const $ = (id) => document.getElementById(id)
|
||||
|
||||
const WORDS = {
|
||||
off: "agent off",
|
||||
not_linked: "not linked",
|
||||
starting: "agent starting…",
|
||||
running: "agent running",
|
||||
elsewhere: "agent running (service)",
|
||||
failed: "agent stopped",
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
let host = ""
|
||||
try {
|
||||
host = s.server ? new URL(s.server).host : ""
|
||||
} catch {}
|
||||
$("host").textContent = host
|
||||
$("host").title = s.server || ""
|
||||
const state = s.agent.state
|
||||
$("agent").dataset.state = state
|
||||
$("agent-text").textContent = WORDS[state] ?? state
|
||||
$("agent").title = state === "failed" ? `Agent stopped: ${s.agent.detail}` : "Agent"
|
||||
}
|
||||
|
||||
function maximized(on) {
|
||||
const b = $("maximize")
|
||||
b.toggleAttribute("data-maximized", on)
|
||||
b.setAttribute("aria-label", on ? "Restore" : "Maximise")
|
||||
b.title = on ? "Restore" : "Maximise"
|
||||
}
|
||||
|
||||
$("minimize").addEventListener("click", () => win.minimize())
|
||||
$("maximize").addEventListener("click", () => win.toggleMaximize())
|
||||
$("close").addEventListener("click", () => win.close())
|
||||
$("menu").addEventListener("click", () => invoke("titlebar_menu"))
|
||||
$("agent").addEventListener("click", () => invoke("titlebar_menu"))
|
||||
|
||||
listen("status", (e) => render(e.payload))
|
||||
listen("maximized", (e) => maximized(!!e.payload))
|
||||
invoke("get_status").then(render)
|
||||
win.isMaximized().then(maximized)
|
||||
+589
-27
@@ -1,54 +1,616 @@
|
||||
/* LLeMbas Desktop's own pages: one set of tokens (Moria dark, Shire light), as on llembas.eu. */
|
||||
/* LLeMbas v2.0.0-beta.1 (a3b0e660cc4d) static/css/tokens.css, copied by scripts/fetch-tokens.sh: do not edit.
|
||||
The font-face rules are left out; the app bundles no font. */
|
||||
/*
|
||||
Design tokens.
|
||||
|
||||
Every colour, space and radius in the application resolves through a variable
|
||||
declared here. Component CSS must never hard-code a hex value -- that is what
|
||||
makes adding a theme a matter of writing one new block rather than auditing
|
||||
every stylesheet.
|
||||
|
||||
Themes are selected with data-theme on <html>. `moria` is the default and is
|
||||
declared on :root so the page is styled even before the theme script runs.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--font-ui: ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
||||
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
/* --- Type ------------------------------------------------------------- */
|
||||
/* The chrome -- buttons, navigation, tabs, labels, headings --
|
||||
is set in `--font-ui`, a monospace face: "Middle-earth on a terminal".
|
||||
What people *read* at length, a reply or a note, stays in `--font-body`,
|
||||
because eighty columns of mono prose is tiring and the look is not worth
|
||||
that. `--font-display` follows the chrome. */
|
||||
--font-ui: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo,
|
||||
Consolas, monospace;
|
||||
--font-display: var(--font-ui);
|
||||
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", "Fira Code",
|
||||
"Cascadia Code", Menlo, Consolas, monospace;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.9375rem;
|
||||
--text-xl: 1.5rem;
|
||||
--text-md: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.375rem;
|
||||
--text-2xl: 1.75rem;
|
||||
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.6;
|
||||
--leading-relaxed: 1.75;
|
||||
|
||||
/* --- Space (4px scale) ------------------------------------------------ */
|
||||
/* Half a step: the gap between the lines of one tool result, the bleed of a
|
||||
mention's highlight. The one place a 2px space is allowed to be named. */
|
||||
--sp-half: 0.125rem;
|
||||
--sp-1: 0.25rem;
|
||||
--sp-2: 0.5rem;
|
||||
--sp-3: 0.75rem;
|
||||
--sp-4: 1rem;
|
||||
--sp-5: 1.25rem;
|
||||
--sp-6: 1.5rem;
|
||||
--sp-8: 2rem;
|
||||
--radius: 2px;
|
||||
--press: 2px;
|
||||
--control-h: 2.5rem;
|
||||
--card-max: 34rem;
|
||||
--sp-10: 2.5rem;
|
||||
--sp-12: 3rem;
|
||||
|
||||
/* --- Radius & shadow -------------------------------------------------- */
|
||||
/* Blocks, not pills. Small on purpose: a corner of 2px reads as
|
||||
square at reading distance and still stops an edge looking cut. Avatars and
|
||||
dots keep `--radius-full`. */
|
||||
--radius-sm: 2px;
|
||||
--radius: 2px;
|
||||
--radius-md: 3px;
|
||||
--radius-lg: 4px;
|
||||
--radius-xl: 6px;
|
||||
--radius-full: 999px;
|
||||
/* Square, on purpose: the sidebar's Chats/Agents switch, which has
|
||||
to read as one solid control rather than as two buttons. */
|
||||
--radius-none: 0;
|
||||
|
||||
/* How far a block button travels when pressed: into its own shadow, which
|
||||
is drawn exactly this far out. The two are one number on purpose. */
|
||||
--press: 2px;
|
||||
|
||||
/* The load-state dot on a model's avatar in the model menu. Its colours are
|
||||
tokens of their own, defaulting to the theme's success and warning, so
|
||||
an instance whose success colour is not green can still say "loaded" in
|
||||
green -- that is what people read a dot beside a name as. */
|
||||
--model-state-size: 0.625rem;
|
||||
--model-state-loaded: var(--success);
|
||||
--model-state-loading: var(--warning);
|
||||
|
||||
/*
|
||||
--- Controls ----------------------------------------------------------
|
||||
Every button, input and select resolves its height from these. That is the
|
||||
whole reason things line up: a row of mixed controls has one height, not
|
||||
whatever each element's padding and font happened to add up to.
|
||||
*/
|
||||
--control-h: 2.25rem;
|
||||
--control-h-sm: 1.75rem;
|
||||
--control-h-lg: 2.75rem;
|
||||
--control-px: 0.75rem;
|
||||
--control-px-sm: 0.5rem;
|
||||
|
||||
/* --- Layout ----------------------------------------------------------- */
|
||||
--sidebar-width: 17.5rem;
|
||||
--inspector-width: 24rem;
|
||||
/* Wider than the inspector because the content is not prose: eighty columns
|
||||
of --font-mono do not fit in 24rem, and a terminal narrower than eighty
|
||||
re-wraps everything a program prints. */
|
||||
--terminal-width: 34rem;
|
||||
--terminal-width-min: 24rem;
|
||||
--thread-max-width: 48rem;
|
||||
--header-height: 3.5rem;
|
||||
|
||||
/* --- The sizes of things -------------------------------------------------
|
||||
Caps and floors that used to be a literal rem in seventy places across
|
||||
three stylesheets -- 20, 22, 24, 26 -- with no way to tell which of them
|
||||
meant the same thing. A handful of roles instead, and a component takes the
|
||||
one that names what it is: a theme can then widen every menu at once, and
|
||||
two popovers cannot drift a rem apart by accident. Control heights are
|
||||
`--control-h`; these are the boxes around them. */
|
||||
--w-field-xs: 8rem; /* a short input: a count, a duration */
|
||||
--w-field-sm: 10rem; /* a short select's floor */
|
||||
--w-field: 14rem; /* a capped field, a label column, a small menu */
|
||||
--w-chip: 16rem; /* a name that truncates: a model, a path */
|
||||
--w-menu-sm: 20rem; /* a compact popover, an attachment chip */
|
||||
--w-menu: 24rem; /* a popover, a results list, a card's column */
|
||||
--w-menu-lg: 28rem; /* a wide popover */
|
||||
--w-dialog: 34rem; /* a dialog with a list in it */
|
||||
--w-prose-sm: 32rem; /* a short reading measure: an empty state */
|
||||
--w-prose: 44rem; /* a reading measure: a lede, a card grid */
|
||||
--h-list: 22rem; /* a scrolling list or menu */
|
||||
--h-block: 24rem; /* a scrolling block inside a card */
|
||||
--h-block-lg: 26rem; /* a reasoning body, a preview */
|
||||
--h-media: 20rem; /* a picture in the transcript */
|
||||
|
||||
/* The live reply's two one-line rows -- the waiting row and the metrics --
|
||||
keep this height whether or not they have anything in them, so their
|
||||
content arriving or leaving never moves the transcript. One line of the
|
||||
small type. */
|
||||
--row-h: calc(var(--text-xs) * var(--leading-normal) + 2 * var(--sp-2));
|
||||
|
||||
/* A dot that marks state: unsaved, unread. */
|
||||
--dot-size: 0.375rem;
|
||||
/* The streaming caret: a thin bar the height of a line. */
|
||||
--caret-bar-w: 0.45rem;
|
||||
/* The drag handle on a panel's edge: how wide, and how far it overhangs the
|
||||
panel so the border itself is inside the target. */
|
||||
--resize-w: 9px;
|
||||
--resize-overhang: 3px;
|
||||
/* A faded control: something present but not the point. */
|
||||
--opacity-dim: 0.45;
|
||||
|
||||
/* What `--header-height` does at the top of the shell, this does at the
|
||||
bottom. The sidebar's footer and the composer sit either side of the same
|
||||
vertical line and both are content-sized, so without this they end at
|
||||
different heights -- and neither can be made to match the other by
|
||||
accident: the footer's height depends on which entries the reader's
|
||||
permissions allow, and the composer's on how much they have typed.
|
||||
|
||||
This began as the fix for a broken *line*: both carried a top border, and
|
||||
the two met the sidebar's edge at different heights. The borders are gone
|
||||
now -- an edge that content scrolls under reads better undrawn than drawn
|
||||
and aligned -- and the token stays, because the two ends of the shell
|
||||
sitting at different heights is visible without any border to prove it.
|
||||
|
||||
A calc of the pieces the footer is actually built from -- four rows at
|
||||
`--control-h`, the gaps between them, and its own padding -- so it stays
|
||||
true if those tokens move. Applied as a `min-height` to both: it holds the
|
||||
footer at full height even for somebody who sees fewer entries, and lifts
|
||||
the composer to meet it. A composer that grows past this as somebody types
|
||||
is expected; the input is getting bigger, and nothing is pretending the
|
||||
sidebar should follow it. */
|
||||
--footer-height: calc(4 * var(--control-h) + 3 * var(--sp-1) + 2 * var(--sp-2));
|
||||
|
||||
/* The terminal's own type. xterm holds this as a number rather than reading
|
||||
it from CSS, so terminal.js parses it back out -- it must stay a plain
|
||||
pixel value. */
|
||||
--terminal-font-size: 13px;
|
||||
|
||||
/*
|
||||
--- Stacking ----------------------------------------------------------
|
||||
These were four bare numbers scattered across two stylesheets, which is
|
||||
fine until something new has to sit between two of them and nobody can
|
||||
say what is already there.
|
||||
*/
|
||||
--z-raised: 5;
|
||||
--z-handle: 10;
|
||||
--z-dropdown: 30;
|
||||
--z-panel: 40;
|
||||
--z-toast: 60;
|
||||
|
||||
--transition-fast: 120ms ease;
|
||||
--transition: 200ms ease;
|
||||
|
||||
/* --- Borders -----------------------------------------------------------
|
||||
A hairline was a literal `1px` in about ninety places, which made it the
|
||||
largest category of hard-coded value left in the codebase -- and the one
|
||||
thing a theme cannot currently change. */
|
||||
--border-w: 1px;
|
||||
--border-w-thick: 2px;
|
||||
--border-w-accent: 3px;
|
||||
|
||||
/* The focus outline's own width. Not `--border-w-thick`, though they are the
|
||||
same number today: an outline is drawn outside the box and takes no space,
|
||||
a border is part of the box and does. Making one of them follow the other
|
||||
means a theme that wants a heavier border gets a heavier focus ring too,
|
||||
which is two decisions tied together by a coincidence. */
|
||||
--outline-w: 2px;
|
||||
/* How far the focus ring stands off the box it rings. */
|
||||
--outline-offset: 2px;
|
||||
|
||||
/* --- Touch --------------------------------------------------------------
|
||||
A control a thumb has to hit is 44px. `--control-h` is 2.25rem, which is
|
||||
36 -- comfortable with a pointer and under every published minimum for a
|
||||
finger -- so the coarse-pointer block at the foot of this file raises the
|
||||
control tokens to this rather than patching components one at a time.
|
||||
Raising the token is the only version that reaches all of them, and it is
|
||||
what `--control-h` exists for. */
|
||||
--tap-min: 2.75rem;
|
||||
/* A tick box, which does not take its size from `--control-h`: the browser
|
||||
draws it and only `width`/`height` move it. */
|
||||
--check-size: 1rem;
|
||||
|
||||
/* --- Controls as a family -----------------------------------------
|
||||
The dropdown's caret is a solid ▾ drawn with two gradients rather than a
|
||||
glyph -- a <select> takes no pseudo-element -- and it sits behind a rule,
|
||||
`[ value │ ▾ ]`, so a select reads as a field with a handle rather than as a
|
||||
button. `--caret-gutter` is everything to the right of the value: the rule,
|
||||
the caret, and the padding between them, so `padding-right` and the
|
||||
background positions are computed from one number. */
|
||||
--caret-w: 0.6rem;
|
||||
--caret-h: 0.3rem;
|
||||
--caret-gutter: 2.25rem;
|
||||
|
||||
/* --- The window's own edges ---------------------------------------------
|
||||
Installed on a phone, the page runs under the notch and the home
|
||||
indicator: base.html asks iOS for `black-translucent`, which is what puts
|
||||
it there, and `viewport-fit=cover` is what lets these resolve to anything
|
||||
but zero. Declared here so no component spells `env()` out -- and so a
|
||||
desktop browser, where all four are 0, costs nothing. */
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-right: env(safe-area-inset-right, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-left: env(safe-area-inset-left, 0px);
|
||||
|
||||
/* --- Breakpoints --------------------------------------------------------
|
||||
A media query cannot read a custom property, so these cannot be *used*
|
||||
here. They are declared anyway so the numbers have one home and a grep for
|
||||
one lands somewhere that says what it means -- and
|
||||
`tests/test_layout_bounds.py` refuses a width in any stylesheet that is not
|
||||
declared here, so a fourth breakpoint invented in passing fails the suite
|
||||
rather than joining the set unannounced.
|
||||
|
||||
--bp-admin 44rem 704px a two-column reference row stacks
|
||||
--bp-narrow 48rem 768px the sidebar becomes a drawer, and controls
|
||||
grow to a thumb's size
|
||||
--bp-wide 64rem 1024px the right-hand panels become overlays */
|
||||
--bp-admin: 44rem;
|
||||
--bp-narrow: 48rem;
|
||||
--bp-wide: 64rem;
|
||||
|
||||
/* --- Motion -------------------------------------------------------------
|
||||
Durations and curves, so the `prefers-reduced-motion` block at the foot of
|
||||
this file keeps covering everything by construction: a literal `1.6s` in a
|
||||
component is a value that block can still neutralise, but one nobody can
|
||||
tune. `--ease-out` is the one to reach for -- something arriving should
|
||||
decelerate; `--ease-spring` overshoots slightly and belongs on a thing
|
||||
that appears, never on a thing that moves under the pointer. */
|
||||
--ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
--ease-in-out: cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--dur-1: 120ms;
|
||||
--dur-2: 200ms;
|
||||
--dur-3: 320ms;
|
||||
--dur-slow: 1.6s;
|
||||
|
||||
/* --- Panel minimums -----------------------------------------------------
|
||||
`api/preferences.py:LAYOUT_BOUNDS` allows four panels' widths to be stored
|
||||
against an account and only two of them -- the two with a drag handle --
|
||||
had a `-min` token or a `min-width` to clamp with. The other two are not
|
||||
draggable, so nothing in the interface could produce a bad value; but the
|
||||
endpoint takes one from anybody signed in, `base.html` applies stored
|
||||
widths to <html> before first paint, and with no clamp a stored 800px
|
||||
sidebar is one nothing in the application can drag back. */
|
||||
--sidebar-width-min: 12.5rem;
|
||||
--inspector-width-min: 17.5rem;
|
||||
|
||||
/* The focus treatment, written once. Three components spelled it out. It
|
||||
resolves --accent-soft at the point of use, so it follows the theme even
|
||||
though it is declared above them. */
|
||||
--ring: 0 0 0 3px var(--accent-soft);
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------------
|
||||
MORIA (default, dark)
|
||||
|
||||
The halls under the mountain on a terminal screen: stone-black surfaces kept
|
||||
near-neutral so the ithildin and mallorn accents carry all the colour, and
|
||||
the doors themselves drawn in moon-letter pixels on the empty screens.
|
||||
---------------------------------------------------------------------------
|
||||
*/
|
||||
:root,
|
||||
:root[data-theme="moria"] {
|
||||
color-scheme: dark;
|
||||
|
||||
/* "Ithildin phosphor": stone-black, with the moon-letters' silver
|
||||
cyan for focus and the primary action and a phosphor leaf for the brand.
|
||||
Contrast measured, not eyeballed -- ink is 15.7:1 on bg, ink-faint 5.4:1 on
|
||||
a card, border-strong 3.3:1. */
|
||||
--bg: #0B0D10;
|
||||
--bg-sunken: #07090B;
|
||||
--surface: #11151A;
|
||||
--surface-raised: #171C22;
|
||||
--surface-hover: #1D232B;
|
||||
--surface-active: #232B34;
|
||||
|
||||
--border: #26303A;
|
||||
/* Bright enough to be an edge (3:1 on a card): the blocky look is drawn in
|
||||
edges, and a 1.9:1 hairline disappears on a dim screen. */
|
||||
--border-strong: #5C6976;
|
||||
|
||||
--ink: #E2E8EC;
|
||||
--ink-muted: #A6B2BC;
|
||||
--ink-faint: #808D98;
|
||||
--ink-inverse: #07090B;
|
||||
|
||||
/* Ithildin: the cool primary, focus and interactive accents. */
|
||||
--accent: #8FD8E8;
|
||||
--accent-hover: #B4E6F1;
|
||||
--accent-ink: #061418;
|
||||
--accent-soft: rgba(143, 216, 232, 0.12);
|
||||
/* Selected text where the glyphs are drawn by something other than the
|
||||
selection -- the composer, whose visible text is its mirror. Stronger
|
||||
than --accent-soft, which is a tint for a background nobody has to find. */
|
||||
--selection: rgba(143, 216, 232, 0.32);
|
||||
|
||||
/* Mallorn: the brand, the assistant's mark, the leaf badges -- phosphor
|
||||
green on the dark, the colour of an old terminal's text. */
|
||||
--leaf: #7EE081;
|
||||
--leaf-hover: #A2EAA3;
|
||||
--leaf-soft: rgba(126, 224, 129, 0.12);
|
||||
|
||||
--danger: #F0735F;
|
||||
--danger-hover: #F4907F;
|
||||
--danger-soft: rgba(240, 115, 95, 0.13);
|
||||
|
||||
/* Teal, a hue away from the leaf: a dot beside a brand badge has to be
|
||||
tellable from it at a glance. */
|
||||
--success: #5FD6B0;
|
||||
--success-soft: rgba(95, 214, 176, 0.13);
|
||||
/* Forge amber. */
|
||||
--warning: #F0A840;
|
||||
--shadow-card: 4px 4px 0 rgba(0, 0, 0, 0.55);
|
||||
--danger: #E5534B;
|
||||
--titlebar-h: 36px;
|
||||
--warning-soft: rgba(240, 168, 64, 0.13);
|
||||
|
||||
--bubble-user: #161C23;
|
||||
--bubble-assistant: transparent;
|
||||
--code-bg: #07090B;
|
||||
--code-border: #26303A;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
--shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 12px 34px rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* Blocks: a hard offset shadow, `--press` out, that a button presses into.
|
||||
On the dark the accent one also glows -- ithildin shows by its light. */
|
||||
--shadow-block: var(--press) var(--press) 0 rgba(0, 0, 0, 0.55);
|
||||
--shadow-block-accent: var(--press) var(--press) 0
|
||||
color-mix(in srgb, var(--accent) 28%, transparent),
|
||||
0 0 22px -6px color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
--shadow-block-lg: 6px 6px 0 rgba(0, 0, 0, 0.6),
|
||||
0 0 40px -12px color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
|
||||
/* The two mostly-empty screens -- a new chat, signing in -- sit on a
|
||||
screen: fine scanlines over a grid that fades out from the middle, lit
|
||||
faintly from the top. Mixed from the accent so a custom theme on this base
|
||||
gets its own colour. Nowhere else: a conversation is read for hours. */
|
||||
--texture-screen:
|
||||
repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.22) 0 1px, transparent 1px 3px),
|
||||
radial-gradient(ellipse 60% 45% at 50% 18%,
|
||||
color-mix(in srgb, var(--accent) 10%, transparent), transparent 70%),
|
||||
radial-gradient(ellipse 75% 65% at 50% 35%, transparent 45%, var(--bg) 92%),
|
||||
linear-gradient(color-mix(in srgb, var(--accent) 5%, transparent) 1px, transparent 1px)
|
||||
0 0 / 24px 24px,
|
||||
linear-gradient(90deg, color-mix(in srgb, var(--accent) 5%, transparent) 1px,
|
||||
transparent 1px) 0 0 / 24px 24px,
|
||||
var(--bg);
|
||||
--glow-title: 0 0 18px color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
|
||||
/* The pixel artwork's colours. Each drawing is a set of named roles
|
||||
(scripts/build_artwork.py); a theme only supplies these. */
|
||||
--art-leaf: #7EE081;
|
||||
--art-leaf-dark: #4FAE5A;
|
||||
--art-leaf-light: #B6F2A6;
|
||||
--art-vein: #D8FFD0;
|
||||
--art-line: #8FD8E8;
|
||||
--art-faint: #4F8794;
|
||||
--art-star: #EAFCFF;
|
||||
--art-glow: rgba(143, 216, 232, 0.55);
|
||||
|
||||
--scrim: rgba(5, 7, 9, 0.7);
|
||||
|
||||
/*
|
||||
The sixteen ANSI colours, for the terminal panel.
|
||||
|
||||
A shell picks its own colours -- `ls --color`, a git diff, htop's meters --
|
||||
and until these existed it got xterm's defaults, which are a different
|
||||
palette from this one and read as a foreign window pasted into the
|
||||
application. Where a slot has an obvious counterpart above it takes it, so
|
||||
an error in the terminal is the same ember as an error anywhere else.
|
||||
|
||||
"black" is not #000: it is what a program picks for a dim background or
|
||||
faint text, and on a near-black surface a true black is invisible.
|
||||
*/
|
||||
--ansi-black: #1A1F26;
|
||||
--ansi-red: #F0735F;
|
||||
--ansi-green: #7EE081;
|
||||
--ansi-yellow: #F0A840;
|
||||
--ansi-blue: #7AA7E8;
|
||||
--ansi-magenta: #C79BE0;
|
||||
--ansi-cyan: #8FD8E8;
|
||||
--ansi-white: #C6CDD4;
|
||||
--ansi-bright-black: #5C6976;
|
||||
--ansi-bright-red: #F4907F;
|
||||
--ansi-bright-green: #A2EAA3;
|
||||
--ansi-bright-yellow: #F5C06E;
|
||||
--ansi-bright-blue: #9CC0F0;
|
||||
--ansi-bright-magenta: #DBBBEB;
|
||||
--ansi-bright-cyan: #B4E6F1;
|
||||
--ansi-bright-white: #E2E8EC;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
/*
|
||||
---------------------------------------------------------------------------
|
||||
SHIRE (light)
|
||||
|
||||
Parchment, ink and moss, printed in one bit: warm and easy to read for a
|
||||
long time, with hard ink edges where Moria glows. Backgrounds are
|
||||
deliberately off-white -- pure #FFF next to the leaf accent reads as
|
||||
clinical rather than as paper.
|
||||
---------------------------------------------------------------------------
|
||||
*/
|
||||
/*
|
||||
`[data-base="shire"]` as well as `[data-theme="shire"]`, and that second
|
||||
selector is what makes a custom theme possible. A custom theme is a handful of
|
||||
token overrides served from /branding.css; everything it does not set has to
|
||||
come from somewhere, and Moria's block above matches bare `:root` so it always
|
||||
applies. Without this a custom *light* theme would be four light colours on
|
||||
near-black surfaces. `<html>` carries both attributes -- see base.html and
|
||||
app.js:applyTheme.
|
||||
*/
|
||||
:root[data-theme="shire"],
|
||||
:root[data-base="shire"] {
|
||||
color-scheme: light;
|
||||
|
||||
/* "1-bit almanac": parchment and ink, a door-green primary, moss
|
||||
for the brand, brass for warnings -- and ink-black block shadows, which is
|
||||
what makes it read as printed. Ink is 13.9:1 on bg, ink-faint 5.6:1. */
|
||||
--bg: #F1EAD6;
|
||||
--bg-sunken: #E7DEC6;
|
||||
--surface: #F8F3E5;
|
||||
--surface-raised: #FCF9F0;
|
||||
--surface-hover: #ECE4CF;
|
||||
--surface-active: #E2D8BD;
|
||||
|
||||
--border: #CBBF9D;
|
||||
/* Ink, not a darker parchment: the blocks are drawn in it. */
|
||||
--border-strong: #2A2E22;
|
||||
|
||||
--ink: #1F2418;
|
||||
--ink-muted: #474D3B;
|
||||
--ink-faint: #5E6250;
|
||||
--ink-inverse: #F8F3E5;
|
||||
|
||||
/* The round door's green: the primary. */
|
||||
--accent: #285F3C;
|
||||
--accent-hover: #1F4D30;
|
||||
--accent-ink: #F8F3E5;
|
||||
--accent-soft: rgba(40, 95, 60, 0.1);
|
||||
--selection: rgba(40, 95, 60, 0.24);
|
||||
|
||||
/* The same mallorn as moss, dark enough to be text on parchment. */
|
||||
--leaf: #3D6B1C;
|
||||
--leaf-hover: #2F5615;
|
||||
--leaf-soft: rgba(61, 107, 28, 0.1);
|
||||
|
||||
--danger: #A3321F;
|
||||
--danger-hover: #862818;
|
||||
--danger-soft: rgba(163, 50, 31, 0.1);
|
||||
|
||||
--success: #2B6A55;
|
||||
--success-soft: rgba(43, 106, 85, 0.1);
|
||||
/* Brass. */
|
||||
--warning: #7E5200;
|
||||
--warning-soft: rgba(126, 82, 0, 0.1);
|
||||
|
||||
--bubble-user: #ECE4CF;
|
||||
--bubble-assistant: transparent;
|
||||
--code-bg: #F1EAD6;
|
||||
--code-border: #CBBF9D;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(42, 46, 34, 0.1);
|
||||
--shadow: 0 4px 14px rgba(42, 46, 34, 0.12);
|
||||
--shadow-lg: 0 12px 34px rgba(42, 46, 34, 0.18);
|
||||
|
||||
/* On paper the block shadow is solid ink: one bit, no blur. */
|
||||
--shadow-block: var(--press) var(--press) 0 var(--border-strong);
|
||||
--shadow-block-accent: var(--press) var(--press) 0 var(--border-strong);
|
||||
--shadow-block-lg: 6px 6px 0 var(--border-strong);
|
||||
|
||||
/* A fine dot dither in place of Moria's scanlines, fading at the edges. */
|
||||
--texture-screen:
|
||||
radial-gradient(ellipse 80% 70% at 50% 30%, transparent 50%, var(--bg) 95%),
|
||||
radial-gradient(color-mix(in srgb, var(--ink) 14%, transparent) 0.8px, transparent 1.1px)
|
||||
0 0 / 6px 6px,
|
||||
var(--bg);
|
||||
--glow-title: none;
|
||||
|
||||
--art-leaf: #4F8A2A;
|
||||
--art-leaf-dark: #2F5E1A;
|
||||
--art-leaf-light: #86B553;
|
||||
--art-vein: #E9F2C8;
|
||||
--art-grass: #5D9A3A;
|
||||
--art-grass-light: #86B553;
|
||||
--art-grass-dark: #3F7428;
|
||||
--art-sky: #CFE3E8;
|
||||
--art-sun: #E8B84A;
|
||||
--art-frame: #5A4425;
|
||||
--art-door: #2F6B3E;
|
||||
--art-door-dark: #24553A;
|
||||
--art-brass: #E8B84A;
|
||||
--art-window: #F3D27A;
|
||||
--art-path: #B89A6A;
|
||||
--art-flower-a: #D9534F;
|
||||
--art-flower-b: #F0C94A;
|
||||
|
||||
--scrim: rgba(31, 36, 24, 0.4);
|
||||
|
||||
/*
|
||||
The same sixteen on parchment, and the reason they are not the Moria set
|
||||
lightened: every one of these has to be readable as *text* on the code
|
||||
background, so the whole palette is darkened rather than brightened.
|
||||
"bright" therefore means more emphatic here, not lighter -- a light
|
||||
terminal theme that made bright yellow actually bright would render it
|
||||
invisible.
|
||||
*/
|
||||
--ansi-black: #1F2418;
|
||||
--ansi-red: #A3321F;
|
||||
--ansi-green: #3D6B1C;
|
||||
--ansi-yellow: #7E5200;
|
||||
--ansi-blue: #2F5D86;
|
||||
--ansi-magenta: #74336A;
|
||||
--ansi-cyan: #2B6A55;
|
||||
--ansi-white: #474D3B;
|
||||
--ansi-bright-black: #6F6A55;
|
||||
--ansi-bright-red: #862818;
|
||||
--ansi-bright-green: #2F5615;
|
||||
--ansi-bright-yellow: #634000;
|
||||
--ansi-bright-blue: #244A6C;
|
||||
--ansi-bright-magenta: #5C2853;
|
||||
--ansi-bright-cyan: #1F5443;
|
||||
--ansi-bright-white: #2A2E22;
|
||||
}
|
||||
|
||||
/*
|
||||
--- Touch -----------------------------------------------------------------
|
||||
A pointer is precise and a finger is about 9mm across, so the same control
|
||||
cannot be the right size for both. `--control-h` is 36px, which is comfortable
|
||||
with a mouse and under every published minimum for a thumb; `--control-h-sm`
|
||||
is 28px, which is a target most people miss.
|
||||
|
||||
Raised here rather than patched per component, because there are upwards of
|
||||
forty of them and the next one added would be 36px again. `--control-h` is
|
||||
what every button, input and select resolves its height from, so one block
|
||||
moves all of them -- which is the reason that token exists.
|
||||
|
||||
Two conditions, either of which is enough.
|
||||
|
||||
`(pointer: coarse)` is the honest one: it is the input device that decides how
|
||||
big a target has to be, and a touchscreen laptop at 1440px has the same thumb
|
||||
as a phone. But a layout below the phone breakpoint is a one-column, drawer-
|
||||
navigated layout whatever is pointing at it -- there is room for bigger
|
||||
controls and every reason to use it -- and that half is also the half a
|
||||
headless browser can be made to prove, which is not nothing: a rule that can
|
||||
only be checked by holding a phone is a rule that quietly rots.
|
||||
*/
|
||||
@media (pointer: coarse), (max-width: 48rem) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #F1EAD6;
|
||||
--surface: #F8F3E5;
|
||||
--surface-hover: #ECE4CF;
|
||||
--border: #CBBF9D;
|
||||
--border-strong: #2A2E22;
|
||||
--ink: #1F2418;
|
||||
--ink-muted: #474D3B;
|
||||
--accent: #285F3C;
|
||||
--accent-hover: #1F4D30;
|
||||
--accent-ink: #F8F3E5;
|
||||
--leaf: #3D6B1C;
|
||||
--warning: #7E5200;
|
||||
--shadow-card: 4px 4px 0 var(--border-strong);
|
||||
--danger: #B3261E;
|
||||
--control-h: var(--tap-min);
|
||||
/* 40px, not the 36 a comfortable pointer gets. A `.btn--sm` is a secondary
|
||||
action, not an unimportant one -- Edit, Enable and Use default are all
|
||||
`.btn--sm`, and on a phone they are the whole interaction. */
|
||||
--control-h-sm: 2.5rem;
|
||||
--control-px: var(--sp-4);
|
||||
--control-px-sm: var(--sp-3);
|
||||
/* A native checkbox is 13-16px whatever the surrounding type is, and no
|
||||
amount of padding on its label changes the box itself. It is the
|
||||
smallest target in the application on a phone by some margin. */
|
||||
--check-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect a stated preference for reduced motion everywhere, at once. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
/* The motion tokens too, for anything that composes a duration rather than
|
||||
declaring one -- a `transition: transform var(--dur-3)` is neutralised by
|
||||
the rule above, but an `animation-delay` built from one is not. */
|
||||
:root {
|
||||
--dur-1: 0.01ms;
|
||||
--dur-2: 0.01ms;
|
||||
--dur-3: 0.01ms;
|
||||
--dur-slow: 0.01ms;
|
||||
--transition-fast: 0.01ms;
|
||||
--transition: 0.01ms;
|
||||
--transition-slow: 0.01ms;
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @codemirror/autocomplete@6.20.4 */
|
||||
import{Annotation as je,StateEffect as O,EditorSelection as v,codePointAt as w,codePointSize as T,fromCodePoint as Ee,Facet as De,combineConfig as He,StateField as ce,Prec as fe,Transaction as Ve,Text as qe,MapMode as J,RangeValue as Ke,RangeSet as ze,CharCategory as Z}from"./cm-state.mjs";import{Direction as Ye,logException as _,showTooltip as Qe,EditorView as L,ViewPlugin as Xe,getTooltip as Re,Decoration as q,WidgetType as Ge,keymap as Pe}from"./cm-view.mjs";import{syntaxTree as $,indentUnit as Je}from"./cm-language.mjs";var U=class{constructor(e,t,n,o){this.state=e,this.pos=t,this.explicit=n,this.view=o,this.abortListeners=[],this.abortOnDocChange=!1}tokenBefore(e){let t=$(this.state).resolveInner(this.pos,-1);for(;t&&e.indexOf(t.name)<0;)t=t.parent;return t?{from:t.from,to:this.pos,text:this.state.sliceDoc(t.from,this.pos),type:t.type}:null}matchBefore(e){let t=this.state.doc.lineAt(this.pos),n=Math.max(t.from,this.pos-250),o=t.text.slice(n-t.from,this.pos-t.from),s=o.search(Le(e,!1));return s<0?null:{from:n+s,to:this.pos,text:o.slice(s)}}get aborted(){return this.abortListeners==null}addEventListener(e,t,n){e=="abort"&&this.abortListeners&&(this.abortListeners.push(t),n&&n.onDocChange&&(this.abortOnDocChange=!0))}};function ge(i){let e=Object.keys(i).join(""),t=/\w/.test(e);return t&&(e=e.replace(/\w/g,"")),`[${t?"\\w":""}${e.replace(/[^\w\s]/g,"\\$&")}]`}function Ze(i){let e=Object.create(null),t=Object.create(null);for(let{label:o}of i){e[o[0]]=!0;for(let s=1;s<o.length;s++)t[o[s]]=!0}let n=ge(e)+ge(t)+"*$";return[new RegExp("^"+n),new RegExp(n)]}function _e(i){let e=i.map(o=>typeof o=="string"?{label:o}:o),[t,n]=e.every(o=>/^\w+$/.test(o.label))?[/\w*$/,/\w+$/]:Ze(e);return o=>{let s=o.matchBefore(n);return s||o.explicit?{from:s?s.from:o.pos,options:e,validFor:t}:null}}function zt(i,e){return t=>{for(let n=$(t.state).resolveInner(t.pos,-1);n;n=n.parent){if(i.indexOf(n.name)>-1)return e(t);if(n.type.isTop)break}return null}}function Yt(i,e){return t=>{for(let n=$(t.state).resolveInner(t.pos,-1);n;n=n.parent){if(i.indexOf(n.name)>-1)return null;if(n.type.isTop)break}return e(t)}}var j=class{constructor(e,t,n,o){this.completion=e,this.source=t,this.match=n,this.score=o}};function E(i){return i.selection.main.from}function Le(i,e){var t;let{source:n}=i,o=e&&n[0]!="^",s=n[n.length-1]!="$";return!o&&!s?i:new RegExp(`${o?"^":""}(?:${n})${s?"$":""}`,(t=i.flags)!==null&&t!==void 0?t:i.ignoreCase?"i":"")}var he=je.define();function et(i,e,t,n){let{main:o}=i.selection,s=t-o.from,l=n-o.from;return{...i.changeByRange(a=>{if(a!=o&&t!=n&&i.sliceDoc(a.from+s,a.from+l)!=i.sliceDoc(t,n))return{range:a};let r=i.toText(e);return{changes:{from:a.from+s,to:n==o.from?a.to:a.from+l,insert:r},range:v.cursor(a.from+s+r.length)}}),scrollIntoView:!0,userEvent:"input.complete"}}var ye=new WeakMap;function tt(i){if(!Array.isArray(i))return i;let e=ye.get(i);return e||ye.set(i,e=_e(i)),e}var H=O.define(),M=O.define(),ee=class{constructor(e){this.pattern=e,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[],this.score=0,this.matched=[];for(let t=0;t<e.length;){let n=w(e,t),o=T(n);this.chars.push(n);let s=e.slice(t,t+o),l=s.toUpperCase();this.folded.push(w(l==s?s.toLowerCase():l,0)),t+=o}this.astral=e.length!=this.chars.length}ret(e,t){return this.score=e,this.matched=t,this}match(e){if(this.pattern.length==0)return this.ret(-100,[]);if(e.length<this.pattern.length)return null;let{chars:t,folded:n,any:o,precise:s,byWord:l}=this;if(t.length==1){let d=w(e,0),R=T(d),C=R==e.length?0:-100;if(d!=t[0])if(d==n[0])C+=-200;else return null;return this.ret(C,[0,R])}let a=e.indexOf(this.pattern);if(a==0)return this.ret(e.length==this.pattern.length?0:-100,[0,this.pattern.length]);let r=t.length,h=0;if(a<0){for(let d=0,R=Math.min(e.length,200);d<R&&h<r;){let C=w(e,d);(C==t[h]||C==n[h])&&(o[h++]=d),d+=T(C)}if(h<r)return null}let c=0,f=0,u=!1,p=0,m=-1,b=-1,k=/[a-z]/.test(e),I=!0;for(let d=0,R=Math.min(e.length,200),C=0;d<R&&f<r;){let x=w(e,d);a<0&&(c<r&&x==t[c]&&(s[c++]=d),p<r&&(x==t[p]||x==n[p]?(p==0&&(m=d),b=d+1,p++):p=0));let W,Y=x<255?x>=48&&x<=57||x>=97&&x<=122?2:x>=65&&x<=90?1:0:(W=Ee(x))!=W.toLowerCase()?1:W!=W.toUpperCase()?2:0;(!d||Y==1&&k||C==0&&Y!=0)&&(t[f]==x||n[f]==x&&(u=!0)?l[f++]=d:l.length&&(I=!1)),C=Y,d+=T(x)}return f==r&&l[0]==0&&I?this.result(-100+(u?-200:0),l,e):p==r&&m==0?this.ret(-200-e.length+(b==e.length?0:-100),[0,b]):a>-1?this.ret(-700-e.length,[a,a+this.pattern.length]):p==r?this.ret(-900-e.length,[m,b]):f==r?this.result(-100+(u?-200:0)+-700+(I?0:-1100),l,e):t.length==2?null:this.result((o[0]?-700:0)+-200+-1100,o,e)}result(e,t,n){let o=[],s=0;for(let l of t){let a=l+(this.astral?T(w(n,l)):1);s&&o[s-1]==l?o[s-1]=a:(o[s++]=l,o[s++]=a)}return this.ret(e-n.length,o)}},te=class{constructor(e){this.pattern=e,this.matched=[],this.score=0,this.folded=e.toLowerCase()}match(e){if(e.length<this.pattern.length)return null;let t=e.slice(0,this.pattern.length),n=t==this.pattern?0:t.toLowerCase()==this.folded?-20Line truncated
|
||||
//# sourceMappingURL=autocomplete.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - codemirror@6.0.2 */
|
||||
import{lineNumbers as p,highlightActiveLineGutter as c,highlightSpecialChars as t,drawSelection as e,dropCursor as h,rectangularSelection as n,crosshairCursor as s,highlightActiveLine as f,keymap as i}from"./cm-view.mjs";import{EditorView as j}from"./cm-view.mjs";import{EditorState as u}from"./cm-state.mjs";import{foldGutter as g,indentOnInput as y,syntaxHighlighting as o,defaultHighlightStyle as r,bracketMatching as d,foldKeymap as S}from"./cm-language.mjs";import{history as a,defaultKeymap as l,historyKeymap as m}from"./cm-commands.mjs";import{highlightSelectionMatches as K,searchKeymap as k}from"./cm-search.mjs";import{closeBrackets as b,autocompletion as w,closeBracketsKeymap as x,completionKeymap as C}from"./cm-autocomplete.mjs";import{lintKeymap as M}from"./cm-lint.mjs";var I=[p(),c(),t(),a(),g(),e(),h(),u.allowMultipleSelections.of(!0),y(),o(r,{fallback:!0}),d(),b(),w(),n(),s(),f(),K(),i.of([...x,...l,...k,...m,...S,...C,...M])],N=[t(),a(),e(),o(r,{fallback:!0}),i.of([...l,...m])];export{j as EditorView,I as basicSetup,N as minimalSetup};
|
||||
//# sourceMappingURL=codemirror.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @codemirror/commands@6.11.1 */
|
||||
import{Annotation as ke,Facet as Se,combineConfig as nt,StateField as rt,Transaction as K,ChangeSet as ot,ChangeDesc as lt,EditorSelection as h,StateEffect as Ae,Text as Be,findClusterBreak as T,countColumn as ee,CharCategory as C}from"./cm-state.mjs";import{EditorView as O,Direction as ct}from"./cm-view.mjs";import{IndentContext as Ce,getIndentation as De,indentString as G,matchBrackets as E,syntaxTree as te,getIndentUnit as Q,indentUnit as st}from"./cm-language.mjs";import{NodeProp as X}from"./lezer-common.mjs";var it=e=>{let{state:t}=e,n=t.doc.lineAt(t.selection.main.from),r=ne(e.state,n.from);return r.line?ut(e):r.block?at(e):!1};function x(e,t){return({state:n,dispatch:r})=>{if(n.readOnly)return!1;let l=e(t,n);return l?(r(n.update(l)),!0):!1}}var ut=x(re,0),An=x(re,1),Bn=x(re,2),ft=x(N,0),Cn=x(N,1),Dn=x(N,2),at=x((e,t)=>N(e,t,dt(t)),0);function ne(e,t){let n=e.languageDataAt("commentTokens",t,1);return n.length?n[0]:{}}var I=50;function ht(e,{open:t,close:n},r,l){let o=e.sliceDoc(r-I,r),c=e.sliceDoc(l,l+I),s=/\s*$/.exec(o)[0].length,i=/^\s*/.exec(c)[0].length,f=o.length-s;if(o.slice(f-t.length,f)==t&&c.slice(i,i+n.length)==n)return{open:{pos:r-s,margin:s&&1},close:{pos:l+i,margin:i&&1}};let u,a;l-r<=2*I?u=a=e.sliceDoc(r,l):(u=e.sliceDoc(r,r+I),a=e.sliceDoc(l-I,l));let d=/^\s*/.exec(u)[0].length,S=/\s*$/.exec(a)[0].length,y=a.length-S-n.length;return u.slice(d,d+t.length)==t&&a.slice(y,y+n.length)==n?{open:{pos:r+d+t.length,margin:/\s/.test(u.charAt(d+t.length))?1:0},close:{pos:l-S-n.length,margin:/\s/.test(a.charAt(y-1))?1:0}}:null}function dt(e){let t=[];for(let n of e.selection.ranges){let r=e.doc.lineAt(n.from),l=n.to<=r.to?r:e.doc.lineAt(n.to);l.from>r.from&&l.from==n.to&&(l=n.to==r.to+1?r:e.doc.lineAt(n.to-1));let o=t.length-1;o>=0&&t[o].to>r.from?t[o].to=l.to:t.push({from:r.from+/^\s*/.exec(r.text)[0].length,to:l.to})}return t}function N(e,t,n=t.selection.ranges){let r=n.map(o=>ne(t,o.from).block);if(!r.every(o=>o))return null;let l=n.map((o,c)=>ht(t,r[c],o.from,o.to));if(e!=2&&!l.every(o=>o))return{changes:t.changes(n.map((o,c)=>l[c]?[]:[{from:o.from,insert:r[c].open+" "},{from:o.to,insert:" "+r[c].close}]))};if(e!=1&&l.some(o=>o)){let o=[];for(let c=0,s;c<l.length;c++)if(s=l[c]){let i=r[c],{open:f,close:u}=s;o.push({from:f.pos-i.open.length,to:f.pos+f.margin},{from:u.pos-u.margin,to:u.pos+i.close.length})}return{changes:o}}return null}function re(e,t,n=t.selection.ranges){let r=[],l=-1;e:for(let{from:o,to:c}of n){let s=r.length,i=1e9,f;for(let u=o;u<=c;){let a=t.doc.lineAt(u);if(f==null&&(f=ne(t,a.from).line,!f))continue e;if(a.from>l&&(o==c||c>a.from)){l=a.from;let d=/^\s*/.exec(a.text)[0].length,S=d==a.length,y=a.text.slice(d,d+f.length)==f?d:-1;d<a.text.length&&d<i&&(i=d),r.push({line:a,comment:y,token:f,indent:d,empty:S,single:!1})}u=a.to+1}if(i<1e9)for(let u=s;u<r.length;u++)r[u].indent<r[u].line.text.length&&(r[u].indent=i);r.length==s+1&&(r[s].single=!0)}if(e!=2&&r.some(o=>o.comment<0&&(!o.empty||o.single))){let o=[];for(let{line:s,token:i,indent:f,empty:u,single:a}of r)(a||!u)&&o.push({from:s.from+f,insert:i+" "});let c=t.changes(o);return{changes:c,selection:t.selection.map(c,1)}}else if(e!=1&&r.some(o=>o.comment>=0)){let o=[];for(let{line:c,comment:s,token:i}of r)if(s>=0){let f=c.from+s,u=f+i.length;c.text[u-c.from]==" "&&u++,o.push({from:f,to:u})}return{changes:o}}return null}var Y=ke.define(),mt=ke.define(),pt=Se.define(),Le=Se.define({combine(e){return nt(e,{minDepth:100,newGroupDelay:500,joinToEvent:(t,n)=>n},{minDepth:Math.max,newGroupDelay:Math.min,joinToEvent:(t,n)=>(r,l)=>t(r,l)||n(r,l)})}}),R=rt.define({create(){return L.empty},update(e,t){let n=t.state.facet(Le),r=t.annotation(Y);if(r){let i=A.fromTransaction(t,r.selection),f=r.side,u=f==0?e.undone:e.done;return i?u=V(u,u.length,n.minDepth,i):u=Te(u,t.startState.selection),new L(f==0?r.rest:u,f==0?u:r.rest)}let l=t.annotation(mt);if((l=="full"||l=="before")&&(e=e.isolate()),t.annotation(K.addToHistory)===!1)return t.changes.empty?e:e.addMapping(t.changes.desc);let o=A.fromTransaction(t),c=t.annotation(K.time),s=t.annotation(K.userEvent);return o?e=e.addChanges(o,c,s,n,t):t.selection&&(e=e.addSelection(t.startState.selection,c,s,n.newGroupDelay)),(l=="full"||l=="after")&&(e=e.isolate()),e},toJSON(e){return{done:e.done.map(t=>t.toJSON()),undone:e.undone.map(t=>t.toJSON())}},fromJSON(e){return new L(e.done.map(A.fromJSON),e.undone.map(A.fromJSON))}});function Ln(e={}){return[R,Le.of(e),O.domEventHandlers({beforeinput(t,n){let r=t.inputType=="historyUndo"?xe:t.inputType=="historyRedo"?Z:null;return r?(t.preventDefault(),r(n)):!1}})]}var xn=R;function P(e,t){return function({state:n,dispatch:r}){if(!t&&n.readOnly)return!1;let l=n.field(R,!1);if(!l)return!1;let o=l.pop(e,n,t);return o?(r(o),!0):!1}}var xe=P(0,!1),Z=P(1,!1),gt=P(0,!0),yt=P(1,!0);function Me(e){return function(t){let n=t.field(R,!1);if(!n)return 0;let r=e==0?n.done:n.undone;return r.length-(r.length&&!r[0].changes?1:0)}}var Mn=Me(0),bn=Me(1);function be(e){return fLine truncated
|
||||
//# sourceMappingURL=commands.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @codemirror/lang-css@6.3.1 */
|
||||
import{ExternalTokenizer as c,LRParser as x,LocalTokenGroup as Y}from"./lezer-lr.mjs";import{styleTags as R,tags as t}from"./lezer-highlight.mjs";var W=148,S=1,q=149,Z=150,h=2,G=151,T=3,_=4,F=152,P=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],E=58,U=40,f=95,N=91,d=45,C=46,V=35,D=37,I=38,B=92,A=10,K=42;function Q(e){return e>=65&&e<=90||e>=97&&e<=122||e>=161}function m(e){return e>=48&&e<=57}function g(e){return m(e)||e>=97&&e<=102||e>=65&&e<=70}var X=(e,a,i)=>(O,r)=>{for(let o=!1,l=0,n=0;;n++){let{next:s}=O;if(Q(s)||s==d||s==f||o&&m(s))!o&&(s!=d||n>0)&&(o=!0),l===n&&s==d&&l++,O.advance();else if(s==B&&O.peek(1)!=A){if(O.advance(),g(O.next)){do O.advance();while(g(O.next));O.next==32&&O.advance()}else O.next>-1&&O.advance();o=!0}else{o&&O.acceptToken(l>=2&&r.canShift(h)?a:s==U?i:e);break}}},L=new c(X(q,h,Z),{contextual:!0}),J=new c(X(G,T,_),{contextual:!0}),H=new c(e=>{if(P.includes(e.peek(-1))){let{next:a}=e;(Q(a)||a==f||a==V||a==C||a==K||a==N||a==E&&Q(e.peek(1))||a==d||a==I)&&e.acceptToken(W)}}),M=new c(e=>{if(!P.includes(e.peek(-1))){let{next:a}=e;if(a==D&&(e.advance(),e.acceptToken(S)),Q(a)){do e.advance();while(Q(e.next)||m(e.next));e.acceptToken(S)}}});function j(e){let a=e.length;return(a==4||a==5||a==7||a==9)&&/^#[a-f\d]+$/i.test(e)?F:-1}var ee=R({"AtKeyword import charset namespace keyframes media supports font-feature-values":t.definitionKeyword,"from to selector scope MatchFlag":t.keyword,NamespaceName:t.namespace,KeyframeName:t.labelName,KeyframeRangeName:t.operatorKeyword,TagName:t.tagName,ClassName:t.className,PseudoClassName:t.constant(t.className),IdName:t.labelName,"FeatureName PropertyName":t.propertyName,AttributeName:t.attributeName,NumberLiteral:t.number,KeywordQuery:t.keyword,UnaryQueryOp:t.operatorKeyword,"CallTag ValueName FontName":t.atom,VariableName:t.variableName,Callee:t.operatorKeyword,Unit:t.unit,"UniversalSelector NestingSelector":t.definitionOperator,"MatchOp CompareOp":t.compareOperator,"ChildOp SiblingOp, LogicOp":t.logicOperator,BinOp:t.arithmeticOperator,Important:t.modifier,Comment:t.blockComment,ColorLiteral:t.color,"ParenthesizedContent StringLiteral":t.string,":":t.punctuation,PseudoOp:t.derefOperator,"; , |":t.separator,"( )":t.paren,"[ ]":t.squareBracket,"{ }":t.brace}),Oe={__proto__:null,lang:44,"nth-child":44,"nth-last-child":44,"nth-of-type":44,"nth-last-of-type":44,dir:44,"host-context":44,if:88,url:158,"url-prefix":158,domain:158,regexp:158},ae={__proto__:null,or:102,and:102,not:112,only:112,layer:212},re={__proto__:null,selector:118,style:124,layer:208},te={__proto__:null,"@import":204,"@media":216,"@charset":220,"@namespace":224,"@keyframes":230,"@supports":242,"@scope":246,"@font-feature-values":252},oe={__proto__:null,to:249},y=x.deserialize({version:14,states:"MrQYQdOOO$TQdOOP$[O`OOO%XQaO'#CfOOQP'#Ce'#CeO%`QdO'#CgO%eQ`O'#CgO%jQaO'#FrO&eQdO'#CkO'XQaO'#CcO'cQdO'#CnOOQP'#ES'#ESOOQP'#ER'#ERO'nQdO'#ETO'yQdO'#E[O'yQdO'#E_OOQP'#Fr'#FrO)`QhO'#FQOOQS'#Fq'#FqOOQS'#FT'#FTQYQdOOO)gQdO'#EeO*vQhO'#EkO)gQdO'#EmO*}QdO'#EoO+YQdO'#ErO*[QhO'#ExO+bQdO'#EzO+mQdO'#E}O+rQaO'#CfO+yQ`O'#EbO,OQ`O'#GPO,ZQdO'#GPQOQ`OOP,eO&jO'#CaPOOO)CAa)CAaOOQP'#Ci'#CiOOQP,59R,59RO%`QdO,59ROOQP'#Cm'#CmOOQP,59V,59VO&eQdO,59VO,pQdO,59YOOQP,5:m,5:mO'nQdO,5:oO'yQdO,5:vO'yQdO,5:xO'yQdO,5:yO'yQdO'#F[O,{Q`O,58}O-TQdO'#EaOOQS,58},58}OOQP'#Cq'#CqOOQO'#EP'#EPOOQP,59Y,59YO-[Q`O,59YO-aQ`O,59YO-fQpO'#EUO-qQdO'#EVO-vQ`O'#EVO-{QpO,5:oO.iQaO,5:vO/PQaO,5:yOOQW'#D]'#D]O0OQhO'#DgO0cQhO,5;lO*[QhO'#DeO0pQ`O'#DnO0uQhO'#D{OOQW'#Fx'#FxOOQS,5;l,5;lO0zQ`O'#DhO1PQ`O'#DkOOQS-E9R-E9ROOQ['#Cv'#CvO1UQdO'#CwO1iQdO'#C|O1|QdO'#DPOOQ['#DQ'#DQO2aQ!pO'#DRO4jQ!jO,5;POOQO'#DW'#DWO-aQ`O'#DVO4zQ!nO'#FuO6}Q`O'#DXO7SQ`O'#D|OOQ['#Fu'#FuO7XQhO'#GSO7gQ`O,5;VO7lQ!bO,5;XOOQS'#Eq'#EqO7tQ`O,5;ZO7yQdO,5;ZOOQO'#Et'#EtO8RQ`O,5;^O8WQhO,5;dO'yQdO'#DjOOQS,5;f,5;fO0zQ`O,5;fO8`QdO,5;fOOQS'#Fc'#FcO8hQdO'#FPO7gQ`O,5;iO8pQdO,5:|O9QQdO'#F^O9_Q`O,5<kO9_Q`O,5<kPOOO'#FS'#FSP9jO&jO,58{POOO,58{,58{OOQP1G.m1G.mOOQP1G.q1G.qOOQP1G.t1G.tO-[Q`O1G.tO-aQ`O1G.tO9uQpO1G0ZO9}QaO1G0bO:eQaO1G0dO:{QaO1G0eO;cQaO,5;vOOQO-E9Y-E9YOOQS1G.i1G.iO;mQ`O,5:{O;rQdO'#EQO;yQdO'#CuOOQO'#EX'#EXOOQO,5:q,5:qO-qQdO,5:qOOQP1G0Z1G0ZO)gQdO1G0ZO<QQ!jO'#D]O<`Q!bO,59xO<hQhO,5:ROOQO'#Dc'#DcOOQO'#Fy'#FyO<cQ!bO,59|O<pQhO'#FdO*[QhO,59zO*[QhO'#FdO=hQhO1G1WOOQS1G1W1G1WO=rQhO,5:PO>mQhO'#DoOOQW,5:Y,5:YOOQW,5:g,5:gOOQW,5:S,5:SO>wQhO,5:VO?cQ!fO'#FvOOQS'#Fv'#FvOOQS'#FV'#FVO@pQdO,59cOOQ[,59c,59cOATQdO,59hOOQ[,59h,59hOAhQdO,59kOOQ[,59k,59kOOQ[,59m,59mO)gQdO,59oOA{QhO'#EgOOQW'#Eg'#EgOBjQ`O1G0kO4sQhO1G0kOOQ[,59q,59qO*[QhO'#DZOOQ[,59s,59sOBoQ#tO,5:hOBzQhO'#F`OCXQ`O,5<nOOQS1G0q1G0qOOQS1G0s1G0sOOQS1G0u1G0uOCdQ`O1G0uOCiQdO'#EuOOQS1G0x1G0xOOQS1G1O1G1OOCtQaO,5:UO7gQ`O1G1QOOQS1G1Q1G1QO0zQ`O1G1QOOQS-E9a-E9aOOQS1G1T1G1TOC{Q!fO1G0hODcQ`O'#EdOOQO1G0h1G0hOOQO,5;x,5;xODhQdO,5;xOOQO-E9[-E9[ODuQ`O1G2VPOOO-E9Q-E9QPOOO1G.g1G.gOOQP7+$`7+$`OOQP7+%u7+%uO)gQdO7+%uOOQS1G0g1G0gOEQQaO'#F}OE[Q`O,5:lOEaQ!fO'#FUOF_QdO'#FtOFiQ`O,59aOOQO1G0]1G0]OFnQ!bO7+%uO)gQdO1G/dOFyQhO1G/hOO
Line truncated
|
||||
//# sourceMappingURL=lang-css.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @codemirror/lang-html@6.4.12 */
|
||||
import{ContextTracker as be,ExternalTokenizer as w,LRParser as he}from"./lezer-lr.mjs";import{styleTags as Te,tags as S}from"./lezer-highlight.mjs";import{parseMixed as Pe}from"./lezer-common.mjs";var xe=55,Ve=1,ye=56,we=2,ve=57,Xe=3,z=4,$e=5,C=6,L=7,F=8,H=9,K=10,qe=11,_e=12,Ce=13,X=58,ke=14,Qe=15,D=59,J=21,Ae=23,ee=24,Ye=25,q=27,te=28,Me=29,Re=32,Ee=35,Ze=37,Be=38,ze=0,De=1,We={area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},Ne={dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},W={dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}};function Ge(e){return e==45||e==46||e==58||e>=65&&e<=90||e==95||e>=97&&e<=122||e>=161}var N=null,G=null,I=0;function _(e,a){let r=e.pos+a;if(I==r&&G==e)return N;let l=e.peek(a),t="";for(;Ge(l);)t+=String.fromCharCode(l),l=e.peek(++a);return G=e,I=r,N=t?t.toLowerCase():l==Ie||l==je?void 0:null}var ae=60,y=62,k=47,Ie=63,je=33,Ue=45;function j(e,a){this.name=e,this.parent=a}var Le=[C,K,L,F,H],Fe=new be({start:null,shift(e,a,r,l){return Le.indexOf(a)>-1?new j(_(l,1)||"",e):e},reduce(e,a){return a==J&&e?e.parent:e},reuse(e,a,r,l){let t=a.type.id;return t==C||t==Ze?new j(_(l,1)||"",e):e},strict:!1}),He=new w((e,a)=>{if(e.next!=ae){e.next<0&&a.context&&e.acceptToken(X);return}e.advance();let r=e.next==k;r&&e.advance();let l=_(e,0);if(l===void 0)return;if(!l)return e.acceptToken(r?Qe:ke);let t=a.context?a.context.name:null;if(r){if(l==t)return e.acceptToken(qe);if(t&&Ne[t])return e.acceptToken(X,-2);if(a.dialectEnabled(ze))return e.acceptToken(_e);for(let s=a.context;s;s=s.parent)if(s.name==l)return;e.acceptToken(Ce)}else{if(l=="script")return e.acceptToken(L);if(l=="style")return e.acceptToken(F);if(l=="textarea")return e.acceptToken(H);if(We.hasOwnProperty(l))return e.acceptToken(K);t&&W[t]&&W[t][l]?e.acceptToken(X,-1):e.acceptToken(C)}},{contextual:!0}),Ke=new w(e=>{for(let a=0,r=0;;r++){if(e.next<0){r&&e.acceptToken(D);break}if(e.next==Ue)a++;else if(e.next==y&&a>=2){r>=3&&e.acceptToken(D,-2);break}else a=0;e.advance()}});function Je(e){for(;e;e=e.parent)if(e.name=="svg"||e.name=="math")return!0;return!1}var et=new w((e,a)=>{if(e.next==k&&e.peek(1)==y){let r=a.dialectEnabled(De)||Je(a.context);e.acceptToken(r?$e:z,2)}else e.next==y&&e.acceptToken(z,1)});function Q(e,a,r){let l=2+e.length;return new w(t=>{for(let s=0,o=0,i=0;;i++){if(t.next<0){i&&t.acceptToken(a);break}if(s==0&&t.next==ae||s==1&&t.next==k||s>=2&&s<l&&t.next==e.charCodeAt(s-2))s++,o++;else if(s==l&&t.next==y){i>o?t.acceptToken(a,-o):t.acceptToken(r,-(o-2));break}else if((t.next==10||t.next==13)&&i){t.acceptToken(a,1);break}else s=o=0;t.advance()}})}var tt=Q("script",xe,Ve),at=Q("style",ye,we),lt=Q("textarea",ve,Xe),rt=Te({"Text RawText IncompleteTag IncompleteCloseTag":S.content,"StartTag StartCloseTag SelfClosingEndTag EndTag":S.angleBracket,TagName:S.tagName,"MismatchedCloseTag/TagName":[S.tagName,S.invalid],AttributeName:S.attributeName,"AttributeValue UnquotedAttributeValue":S.attributeValue,Is:S.definitionOperator,"EntityReference CharacterReference":S.character,Comment:S.blockComment,ProcessingInst:S.processingInstruction,DoctypeDecl:S.documentMeta}),le=he.deserialize({version:14,states:",xOVO!rOOO!ZQ#tO'#CrO!`Q#tO'#C{O!eQ#tO'#DOO!jQ#tO'#DRO!oQ#tO'#DTO!tOaO'#CqO#PObO'#CqO#[OdO'#CqO$kO!rO'#CqOOO`'#Cq'#CqO$rO$fO'#DUO$zQ#tO'#DWO%PQ#tO'#DXOOO`'#Dl'#DlOOO`'#DZ'#DZQVO!rOOO%UQ&rO,59^O%aQ&rO,59gO%lQ&rO,59jO%wQ&rO,59mO&SQ&rO,59oOOOa'#D_'#D_O&_OaO'#CyO&jOaO,59]OOOb'#D`'#D`O&rObO'#C|O&}ObO,59]OOOd'#Da'#DaO'VOdO'#DPO'bOdO,59]OOO`'#Db'#DbO'jO!rO,59]O'qQ#tO'#DSOOO`,59],59]OOOp'#Dc'#DcO'vO$fO,59pOOO`,59p,59pO(OQ#|O,59rO(TQ#|O,59sOOO`-E7X-E7XO(YQ&rO'#CtOOQW'#D['#D[O(hQ&rO1G.xOOOa1G.x1G.xOOO`1G/Z1G/ZO(sQ&rO1G/ROOOb1G/R1G/RO)OQ&rO1G/UOOOd1G/U1G/UO)ZQ&rO1G/XOOO`1G/X1G/XO)fQ&rO1G/ZOOOa-E7]-E7]O)qQ#tO'#CzOOO`1G.w1G.wOOOb-E7^-E7^O)vQ#tO'#C}OOOd-E7_-E7_O){Q#tO'#DQOOO`-E7`-E7`O*QQ#|O,59nOOOp-E7a-E7aOOO`1G/[1G/[OOO`1G/^1G/^OOO`1G/_1G/_O*VQ,UO,59`OOQW-E7Y-E7YOOOa7+$d7+$dOOO`7+$u7+$uOOOb7+$m7+$mOOOd7+$p7+$pOOO`7+$s7+$sO*bQ#|O,59fO*gQ#|O,59iO*lQ#|O,59lOOO`1G/Y1G/YO*qO7[O'#CwO+SOMhO'#CwOOQW1G.z1G.zOOO`1G/Q1G/QOOO`1G/T1G/TOOO`1G/W1G/WOOOO'#D]'#D]O+eO7[O,59cOOQW,59c,59cOOOO'#D^'#D^O+vOMhO,59cOOOO-E7Z-E7ZOOQW1G.}1G.}OOOO-E7[-E7[",stateData:",c~O!_OS~OUSOVPOWQOXROYTO[]O][O^^O_^Oa^Ob^Oc^Od^Oy^O|_O!eZO~OgaO~OgbO~OgcO~OgdO~OgeO~O!XfOPmP![mP~O!YiOQpP![pP~O!ZlORsP![sP~OUSOVPOWQOXROYTOZqO[]O][O^^O_^Oa^Ob^Oc^Od^Oy^O!eZO~O![rO~P#gO!]sO!fuO~OgvO~OgwO~OS|OT}OiyO~OS!POT}OiyO~OS!ROT}OiyO~OS!TOT}OiyO~OS}OT}Line truncated
|
||||
//# sourceMappingURL=lang-html.bundle.mjs.map
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
/* esm.sh - @codemirror/lang-javascript@6.2.5 */
|
||||
import{ContextTracker as E,ExternalTokenizer as S,LRParser as A,LocalTokenGroup as k}from"./lezer-lr.mjs";import{styleTags as C,tags as e}from"./lezer-highlight.mjs";var I=317,J=318,w=1,D=2,N=3,K=4,B=319,F=321,H=322,OO=5,aO=6,QO=0,m=[9,10,11,12,13,32,133,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288],v=125,rO=59,u=47,eO=42,iO=43,$O=45,tO=60,nO=44,lO=63,ZO=46,oO=91,PO=new E({start:!1,shift(O,a){return a==OO||a==aO||a==F?O:a==H},strict:!1}),sO=new S((O,a)=>{let{next:Q}=O;(Q==v||Q==-1||a.context)&&O.acceptToken(B)},{contextual:!0,fallback:!0}),pO=new S((O,a)=>{let{next:Q}=O,r;m.indexOf(Q)>-1||Q==u&&((r=O.peek(1))==u||r==eO)||Q!=v&&Q!=rO&&Q!=-1&&!a.context&&O.acceptToken(I)},{contextual:!0}),XO=new S((O,a)=>{O.next==oO&&!a.context&&O.acceptToken(J)},{contextual:!0}),SO=new S((O,a)=>{let{next:Q}=O;if(Q==iO||Q==$O){if(O.advance(),Q==O.next){O.advance();let r=!a.context&&a.canShift(w);O.acceptToken(r?w:D)}}else Q==lO&&O.peek(1)==ZO&&(O.advance(),O.advance(),(O.next<48||O.next>57)&&O.acceptToken(N))},{contextual:!0});function f(O,a){return O>=65&&O<=90||O>=97&&O<=122||O==95||O>=192||!a&&O>=48&&O<=57}var cO=new S((O,a)=>{if(O.next!=tO||!a.dialectEnabled(QO)||(O.advance(),O.next==u))return;let Q=0;for(;m.indexOf(O.next)>-1;)O.advance(),Q++;if(f(O.next,!0)){for(O.advance(),Q++;f(O.next,!1);)O.advance(),Q++;for(;m.indexOf(O.next)>-1;)O.advance(),Q++;if(O.next==nO)return;for(let r=0;;r++){if(r==7){if(!f(O.next,!0))return;break}if(O.next!="extends".charCodeAt(r))break;O.advance(),Q++}}O.acceptToken(K,-Q)}),dO=C({"get set async static":e.modifier,"for while do if else switch try catch finally return throw break continue default case defer":e.controlKeyword,"in of await yield void typeof delete instanceof as satisfies":e.operatorKeyword,"let var const using function class extends":e.definitionKeyword,"import export from":e.moduleKeyword,"with debugger new":e.keyword,TemplateString:e.special(e.string),super:e.atom,BooleanLiteral:e.bool,this:e.self,null:e.null,Star:e.modifier,VariableName:e.variableName,"CallExpression/VariableName TaggedTemplateExpression/VariableName":e.function(e.variableName),VariableDefinition:e.definition(e.variableName),Label:e.labelName,PropertyName:e.propertyName,PrivatePropertyName:e.special(e.propertyName),"CallExpression/MemberExpression/PropertyName":e.function(e.propertyName),"FunctionDeclaration/VariableDefinition":e.function(e.definition(e.variableName)),"ClassDeclaration/VariableDefinition":e.definition(e.className),"NewExpression/VariableName":e.className,PropertyDefinition:e.definition(e.propertyName),PrivatePropertyDefinition:e.definition(e.special(e.propertyName)),UpdateOp:e.updateOperator,"LineComment Hashbang":e.lineComment,BlockComment:e.blockComment,Number:e.number,String:e.string,Escape:e.escape,ArithOp:e.arithmeticOperator,LogicOp:e.logicOperator,BitOp:e.bitwiseOperator,CompareOp:e.compareOperator,RegExp:e.regexp,Equals:e.definitionOperator,Arrow:e.function(e.punctuation),": Spread":e.punctuation,"( )":e.paren,"[ ]":e.squareBracket,"{ }":e.brace,"InterpolationStart InterpolationEnd":e.special(e.brace),".":e.derefOperator,", ;":e.separator,"@":e.meta,TypeName:e.typeName,TypeDefinition:e.definition(e.typeName),"type enum interface implements namespace module declare":e.definitionKeyword,"abstract global Privacy readonly override":e.modifier,"is keyof unique infer asserts":e.operatorKeyword,JSXAttributeValue:e.attributeValue,JSXText:e.content,"JSXStartTag JSXStartCloseTag JSXSelfCloseEndTag JSXEndTag":e.angleBracket,"JSXIdentifier JSXNameSpacedName":e.tagName,"JSXAttribute/JSXIdentifier JSXAttribute/JSXNameSpacedName":e.attributeName,"JSXBuiltin/JSXIdentifier":e.standard(e.tagName)}),gO={__proto__:null,export:20,as:25,from:33,default:36,async:41,function:42,in:52,out:55,const:56,extends:60,this:64,true:72,false:72,null:84,void:88,typeof:92,super:108,new:142,delete:154,yield:163,await:167,class:172,public:237,private:237,protected:237,readonly:239,instanceof:258,satisfies:261,import:294,keyof:351,unique:355,infer:361,asserts:397,is:399,abstract:419,implements:421,type:423,let:426,var:428,using:431,interface:437,enum:441,namespace:447,module:449,declare:453,global:457,defer:473,for:478,of:487,while:490,with:494,do:498,if:502,else:504,switch:508,case:514,try:520,catch:524,finally:528,return:532,throw:536,break:540,continue:544,debugger:548},bO={__proto__:null,async:129,get:131,set:133,declare:195,public:197,private:197,protected:197,static:199,abstract:201,override:203,readonly:209,accessor:211,new:403},fO={__proto__:null,"<":193},y=A.deserialize({version:14,states:"$GSQ%TQlOOO%[QlOOO'_QpOOP(lO`OOO*zQ!0MxO'#CiO+RO#tO'#CjO+aO&jO'#CjO+oO#@ItO'#DaO.QQlO'#DgO.bQlO'#DrO%[QlO'#DzO0fQlO'#ESOOQ!0Lf'#E['#E[O1PQ`O'#EXOOQO'#Ep'#EpOOQO'#Im'#ImO1XQ`O'#GtO1dQ`O'#EoO1iQ`O'#EoO3hQ!0MxO'#JsO6[Q!0MxO'#JtO6uQ`O'#F^O6zQ,UO'#FuOOQ!0Lf'#Fg'#FgO7VO7dO'#FgO9XQMhO'#F}O9`Q`O'#F|OOQ!0Lf'#Jt'#JtOOQ!0Lb'#Js'#JsO9eQ`O'#GxOOQ['#K`'#K`O9pQ`O'#IZO9uQ!0LrO'#I[OOQ['#Ja'#JaOOQ['
Line truncated
|
||||
\${}
|
||||
} catch (\${error}) {
|
||||
\${}
|
||||
}`,{label:"try",detail:"/ catch block",type:"keyword"}),Z("if (${}) {\n ${}\n}",{label:"if",detail:"block",type:"keyword"}),Z(`if (\${}) {
|
||||
\${}
|
||||
} else {
|
||||
\${}
|
||||
}`,{label:"if",detail:"/ else block",type:"keyword"}),Z(`class \${name} {
|
||||
constructor(\${params}) {
|
||||
\${}
|
||||
}
|
||||
}`,{label:"class",detail:"definition",type:"keyword"}),Z('import {${names}} from "${module}"\n${}',{label:"import",detail:"named",type:"keyword"}),Z('import ${name} from "${module}"\n${}',{label:"import",detail:"default",type:"keyword"})],WO=G.concat([Z("interface ${name} {\n ${}\n}",{label:"interface",detail:"definition",type:"keyword"}),Z("type ${name} = ${type}",{label:"type",detail:"definition",type:"keyword"}),Z("enum ${name} {\n ${}\n}",{label:"enum",detail:"definition",type:"keyword"})]),q=new RO,z=new Set(["Script","Block","FunctionExpression","FunctionDeclaration","ArrowFunction","MethodDeclaration","ForStatement"]);function c(O){return(a,Q)=>{let r=a.node.getChild("VariableDefinition");return r&&Q(r,O),!0}}var GO=["FunctionDeclaration"],zO={FunctionDeclaration:c("function"),ClassDeclaration:c("class"),ClassExpression:()=>!0,EnumDeclaration:c("constant"),TypeAliasDeclaration:c("type"),NamespaceDeclaration:c("namespace"),VariableDefinition(O,a){O.matchContext(GO)||a(O,"variable")},TypeDefinition(O,a){a(O,"type")},__proto__:null};function T(O,a){let Q=q.get(a);if(Q)return Q;let r=[],i=!0;function t($,n){let o=O.sliceString($.from,$.to);r.push({label:o,type:n})}return a.cursor(UO.IncludeAnonymous).iterate($=>{if(i)i=!1;else if($.name){let n=zO[$.name];if(n&&n($,t)||z.has($.name))return!1}else if($.to-$.from>8192){for(let n of T(O,$.node))r.push(n);return!1}}),q.set(a,r),r}var g=/^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/,h=["TemplateString","String","RegExp","LineComment","BlockComment","VariableDefinition","TypeDefinition","Label","PropertyDefinition","PropertyName","PrivatePropertyDefinition","PrivatePropertyName","JSXText","JSXAttributeValue","JSXOpenTag","JSXCloseTag","JSXSelfClosingTag",".","?."];function TO(O){let a=_(O.state).resolveInner(O.pos,-1);if(h.indexOf(a.name)>-1)return null;let Q=a.name=="VariableName"||a.to-a.from<20&&g.test(O.state.sliceDoc(a.from,a.to));if(!Q&&!O.explicit)return null;let r=[];for(let i=a;i;i=i.parent)z.has(i.name)&&(r=r.concat(T(O.state.doc,i)));return{options:r,from:Q?a.from:O.pos,validFor:g}}function x(O,a,Q){var r;let i=[];for(;;){let t=a.firstChild,$;if(t?.name=="VariableName")return i.push(O(t)),{path:i.reverse(),name:Q};if(t?.name=="MemberExpression"&&((r=$=t.lastChild)===null||r===void 0?void 0:r.name)=="PropertyName")i.push(O($)),a=t;else return null}}function VO(O){let a=r=>O.state.doc.sliceString(r.from,r.to),Q=_(O.state).resolveInner(O.pos,-1);return Q.name=="PropertyName"?x(a,Q.parent,a(Q)):(Q.name=="."||Q.name=="?.")&&Q.parent.name=="MemberExpression"?x(a,Q.parent,""):h.indexOf(Q.name)>-1?null:Q.name=="VariableName"||Q.to-Q.from<20&&g.test(a(Q))?{path:[],name:a(Q)}:Q.name=="MemberExpression"?x(a,Q,""):O.explicit?{path:[],name:""}:null}function LO(O,a){let Q=O,r=[],i=new Set;for(let t=0;;t++){for(let n of(Object.getOwnPropertyNames||Object.keys)(O)){if(!/^[a-zA-Z_$\xaa-\uffdc][\w$\xaa-\uffdc]*$/.test(n)||i.has(n))continue;i.add(n);let o;try{o=Q[n]}catch{continue}r.push({label:n,type:typeof o=="function"?/^[A-Z]/.test(n)?"class":a?"function":"method":a?"variable":"property",boost:-t})}let $=Object.getPrototypeOf(O);if(!$)return r;O=$}}function ia(O){let a=new Map;return Q=>{let r=VO(Q);if(!r)return null;let i=O;for(let $ of r.path)if(i=i[$],!i)return null;let t=a.get(i);return t||a.set(i,t=LO(i,!r.path.length)),{from:Q.pos-r.name.length,options:t,validFor:g}}}var p=mO.define({name:"javascript",parser:y.configure({props:[uO.add({IfStatement:Y({except:/^\s*({|else\b)/}),TryStatement:Y({except:/^\s*({|catch\b|finally\b)/}),LabeledStatement:YO,SwitchBody:O=>{let a=O.textAfter,Q=/^\s*\}/.test(a),r=/^\s*(case|default)\b/.test(a);return O.baseIndent+(Q?0:r?1:2)*O.unit},Block:xO({closing:"}"}),ArrowFunction:O=>O.baseIndent+O.unit,"TemplateString BlockComment":()=>null,"Statement Property":Y({except:/^\s*{/}),JSXElement(O){let a=/^\s*<\//.test(O.textAfter);return O.lineIndent(O.node.from)+(a?0:O.unit)},JSXEscape(O){let a=/\s*\}/.test(O.textAfter);return O.lineIndent(O.node.from)+(a?0:O.unit)},"JSXOpenTag JSXSelfClosingTag"(O){return O.column(O.node.from)+O.unit}}),_O.add({"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType":hO,BlockComment(O){return{from:O.from+2,to:O.to-2}},JSXElement(O){let a=O.firstChild;if(!a||a.name=="JSXSelfClosingTag")return null;let Q=O.lastChild;return{from:a.to,to:Q.type.isError?O.to:Q.from}},"JSXSelfClosingTag JSXOpenTag"(O){var a;let Q=(a=O.firstChild)===null||a===void 0?void 0:a.nextSibling,r=O.lastChild;return!Q||Q.type.isError?null:{from:Q.to,to:r.type.isError?O.to:r.from}}})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"`"]},commentTokens:{line:"//",block:{open:"/*",close:"*/"}},indentOnInput:/^\s*(?:case |default:|\{|\}|<\/)$/,wordChars:"$"}}),V={test:O=>/^JSX/.test(O.name),facet:jO({commentTokens:{block:{open:"{/*",close:"*/}"}}})},MO=p.configure({dialect:"ts"},"typescript"),EO=p.configure({dialect:"jsx",props:[W.add(O=>O.isTop?[V]:void 0)]}),AO=p.configure({dialect:"jsx ts",props:[W.ad
Line truncated
|
||||
//# sourceMappingURL=lang-javascript.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @codemirror/lang-json@6.0.2 */
|
||||
import{LRParser as n}from"./lezer-lr.mjs";import{styleTags as s,tags as r}from"./lezer-highlight.mjs";var P=s({String:r.string,Number:r.number,"True False":r.bool,PropertyName:r.propertyName,Null:r.null,", :":r.separator,"[ ]":r.squareBracket,"{ }":r.brace}),o=n.deserialize({version:14,states:"$bOVQPOOOOQO'#Cb'#CbOnQPO'#CeOvQPO'#ClOOQO'#Cr'#CrQOQPOOOOQO'#Cg'#CgO}QPO'#CfO!SQPO'#CtOOQO,59P,59PO![QPO,59PO!aQPO'#CuOOQO,59W,59WO!iQPO,59WOVQPO,59QOqQPO'#CmO!nQPO,59`OOQO1G.k1G.kOVQPO'#CnO!vQPO,59aOOQO1G.r1G.rOOQO1G.l1G.lOOQO,59X,59XOOQO-E6k-E6kOOQO,59Y,59YOOQO-E6l-E6l",stateData:"#O~OeOS~OQSORSOSSOTSOWQO_ROgPO~OVXOgUO~O^[O~PVO[^O~O]_OVhX~OVaO~O]bO^iX~O^dO~O]_OVha~O]bO^ia~O",goto:"!kjPPPPPPkPPkqwPPPPk{!RPPP!XP!e!hXSOR^bQWQRf_TVQ_Q`WRg`QcZRicQTOQZRQe^RhbRYQR]R",nodeNames:"\u26A0 JsonText True False Null Number String } { Object Property PropertyName : , ] [ Array",maxTerm:25,nodeProps:[["isolate",-2,6,11,""],["openedBy",7,"{",14,"["],["closedBy",8,"}",15,"]"]],propSources:[P],skippedNodes:[0],repeatNodeCount:2,tokenData:"(|~RaXY!WYZ!W]^!Wpq!Wrs!]|}$u}!O$z!Q!R%T!R![&c![!]&t!}#O&y#P#Q'O#Y#Z'T#b#c'r#h#i(Z#o#p(r#q#r(w~!]Oe~~!`Wpq!]qr!]rs!xs#O!]#O#P!}#P;'S!];'S;=`$o<%lO!]~!}Og~~#QXrs!]!P!Q!]#O#P!]#U#V!]#Y#Z!]#b#c!]#f#g!]#h#i!]#i#j#m~#pR!Q![#y!c!i#y#T#Z#y~#|R!Q![$V!c!i$V#T#Z$V~$YR!Q![$c!c!i$c#T#Z$c~$fR!Q![!]!c!i!]#T#Z!]~$rP;=`<%l!]~$zO]~~$}Q!Q!R%T!R![&c~%YRT~!O!P%c!g!h%w#X#Y%w~%fP!Q![%i~%nRT~!Q![%i!g!h%w#X#Y%w~%zR{|&T}!O&T!Q![&Z~&WP!Q![&Z~&`PT~!Q![&Z~&hST~!O!P%c!Q![&c!g!h%w#X#Y%w~&yO[~~'OO_~~'TO^~~'WP#T#U'Z~'^P#`#a'a~'dP#g#h'g~'jP#X#Y'm~'rOR~~'uP#i#j'x~'{P#`#a(O~(RP#`#a(U~(ZOS~~(^P#f#g(a~(dP#i#j(g~(jP#X#Y(m~(rOQ~~(wOW~~(|OV~",tokenizers:[0],topRules:{JsonText:[0,1]},tokenPrec:0});import{LRLanguage as Q,indentNodeProp as i,continuedIndent as a,foldNodeProp as c,foldInside as g,LanguageSupport as p}from"./cm-language.mjs";var h=()=>t=>{try{JSON.parse(t.state.doc.toString())}catch(O){if(!(O instanceof SyntaxError))throw O;let e=m(O,t.state.doc);return[{from:e,message:O.message,severity:"error",to:e}]}return[]};function m(t,O){let e;return(e=t.message.match(/at position (\d+)/))?Math.min(+e[1],O.length):(e=t.message.match(/at line (\d+) column (\d+)/))?Math.min(O.line(+e[1]).from+ +e[2]-1,O.length):0}var l=Q.define({name:"json",parser:o.configure({props:[i.add({Object:a({except:/^\s*\}/}),Array:a({except:/^\s*\]/})}),c.add({"Object Array":g})]}),languageData:{closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/}});function S(){return new p(l)}export{S as json,l as jsonLanguage,h as jsonParseLinter};
|
||||
//# sourceMappingURL=lang-json.bundle.mjs.map
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/* esm.sh - @codemirror/lang-markdown@6.5.2 */
|
||||
import{EditorSelection as N,countColumn as U,Prec as zt,EditorState as Rt}from"./cm-state.mjs";import{EditorView as Tt,keymap as Dt}from"./cm-view.mjs";import{defineLanguageFacet as Ft,foldNodeProp as ut,indentNodeProp as Xt,languageDataProp as $t,foldService as Ut,syntaxTree as j,Language as jt,LanguageDescription as at,ParseContext as _t,indentUnit as qt,LanguageSupport as lt}from"./cm-language.mjs";import{CompletionContext as Qt}from"./cm-autocomplete.mjs";import{NodeType as J,NodeProp as D,NodeSet as Oe,Tree as x,Parser as bt,IterMode as Ce,parseMixed as wt}from"./lezer-common.mjs";import{styleTags as ze,tags as m,Tag as St}from"./lezer-highlight.mjs";var Q=class t{static create(e,r,n,s,i){let o=s+(s<<8)+e+(r<<4)|0;return new t(e,r,n,o,i,[],[])}constructor(e,r,n,s,i,o,a){this.type=e,this.value=r,this.from=n,this.hash=s,this.end=i,this.children=o,this.positions=a,this.blankFirstLine=!1,this.hashProp=[[D.contextHash,s]]}addChild(e,r){e.prop(D.contextHash)!=this.hash&&(e=new x(e.type,e.children,e.positions,e.length,this.hashProp)),this.children.push(e),this.positions.push(r)}toTree(e,r=this.end){let n=this.children.length-1;return n>=0&&(r=Math.max(r,this.positions[n]+this.children[n].length+this.from)),new x(e.types[this.type],this.children,this.positions,r-this.from).balance({makeTree:(s,i,o)=>new x(J.none,s,i,o,this.hashProp)})}},u;(function(t){t[t.Document=1]="Document",t[t.CodeBlock=2]="CodeBlock",t[t.FencedCode=3]="FencedCode",t[t.Blockquote=4]="Blockquote",t[t.HorizontalRule=5]="HorizontalRule",t[t.BulletList=6]="BulletList",t[t.OrderedList=7]="OrderedList",t[t.ListItem=8]="ListItem",t[t.ATXHeading1=9]="ATXHeading1",t[t.ATXHeading2=10]="ATXHeading2",t[t.ATXHeading3=11]="ATXHeading3",t[t.ATXHeading4=12]="ATXHeading4",t[t.ATXHeading5=13]="ATXHeading5",t[t.ATXHeading6=14]="ATXHeading6",t[t.SetextHeading1=15]="SetextHeading1",t[t.SetextHeading2=16]="SetextHeading2",t[t.HTMLBlock=17]="HTMLBlock",t[t.LinkReference=18]="LinkReference",t[t.Paragraph=19]="Paragraph",t[t.CommentBlock=20]="CommentBlock",t[t.ProcessingInstructionBlock=21]="ProcessingInstructionBlock",t[t.Escape=22]="Escape",t[t.Entity=23]="Entity",t[t.HardBreak=24]="HardBreak",t[t.Emphasis=25]="Emphasis",t[t.StrongEmphasis=26]="StrongEmphasis",t[t.Link=27]="Link",t[t.Image=28]="Image",t[t.InlineCode=29]="InlineCode",t[t.HTMLTag=30]="HTMLTag",t[t.Comment=31]="Comment",t[t.ProcessingInstruction=32]="ProcessingInstruction",t[t.Autolink=33]="Autolink",t[t.HeaderMark=34]="HeaderMark",t[t.QuoteMark=35]="QuoteMark",t[t.ListMark=36]="ListMark",t[t.LinkMark=37]="LinkMark",t[t.EmphasisMark=38]="EmphasisMark",t[t.CodeMark=39]="CodeMark",t[t.CodeText=40]="CodeText",t[t.CodeInfo=41]="CodeInfo",t[t.LinkTitle=42]="LinkTitle",t[t.LinkLabel=43]="LinkLabel",t[t.URL=44]="URL"})(u||(u={}));var te=class{constructor(e,r){this.start=e,this.content=r,this.marks=[],this.parsers=[]}},re=class{constructor(){this.text="",this.baseIndent=0,this.basePos=0,this.depth=0,this.markers=[],this.pos=0,this.indent=0,this.next=-1}forward(){this.basePos>this.pos&&this.forwardInner()}forwardInner(){let e=this.skipSpace(this.basePos);this.indent=this.countIndent(e,this.pos,this.indent),this.pos=e,this.next=e==this.text.length?-1:this.text.charCodeAt(e)}skipSpace(e){return R(this.text,e)}reset(e){for(this.text=e,this.baseIndent=this.basePos=this.pos=this.indent=0,this.forwardInner(),this.depth=1;this.markers.length;)this.markers.pop()}moveBase(e){this.basePos=e,this.baseIndent=this.countIndent(e,this.pos,this.indent)}moveBaseColumn(e){this.baseIndent=e,this.basePos=this.findColumn(e)}addMarker(e){this.markers.push(e)}countIndent(e,r=0,n=0){for(let s=r;s<e;s++)n+=this.text.charCodeAt(s)==9?4-n%4:1;return n}findColumn(e){let r=0;for(let n=0;r<this.text.length&&n<e;r++)n+=this.text.charCodeAt(r)==9?4-n%4:1;return r}scrub(){if(!this.baseIndent)return this.text;let e="";for(let r=0;r<this.basePos;r++)e+=" ";return e+this.text.slice(this.basePos)}};function Ae(t,e,r){if(r.pos==r.text.length||t!=e.block&&r.indent>=e.stack[r.depth+1].value+r.baseIndent)return!0;if(r.indent>=r.baseIndent+4)return!1;let n=(t.type==u.OrderedList?ce:pe)(r,e,!1);return n>0&&(t.type!=u.BulletList||de(r,e,!1)<0)&&r.text.charCodeAt(r.pos+n-1)==t.value}var Re={[u.Blockquote](t,e,r){return r.next!=62?!1:(r.markers.push(g(u.QuoteMark,e.lineStart+r.pos,e.lineStart+r.pos+1)),r.moveBase(r.pos+(A(r.text.charCodeAt(r.pos+1))?2:1)),t.end=e.lineStart+r.text.length,!0)},[u.ListItem](t,e,r){return(r.next<0?t.blankFirstLine&&t.children.length==1:r.indent<r.baseIndent+t.value)?!1:(r.moveBaseColumn(r.baseIndent+t.value),!0)},[u.OrderedList]:Ae,[u.BulletList]:Ae,[u.Document](){return!0}};function A(t){return t==32||t==9||t==10||t==13}function R(t,e=0){for(;e<t.length&&A(t.charCodeAt(e));)e++;return e}function xe(t,e,r){for(;e>r&&A(t.charCodeAt(e-1));)e--;return e}function Te(t){if(t.next!=96&&t.next!=126)return-1;let e=t.pos+1;for(;e<t.text.length&&t.text.charCodeAt(e)==t.next;)e++;if(e<t.pos+3)return-1;if(t.next==96){for(let r=e;r<tLine truncated
|
||||
`+r.scrub(),i=this.advance(s);return i>-1&&i<s.length?this.complete(e,n,i):!1}finish(e,r){return(this.stage==2||this.stage==3)&&R(r.content,this.pos)==r.content.length?this.complete(e,r,r.content.length):!1}complete(e,r,n){return e.addLeafElement(r,g(u.LinkReference,this.start,this.start+n,this.elts)),!0}nextStage(e){return e?(this.pos=e.to-this.start,this.elts.push(e),this.stage++,!0):(e===!1&&(this.stage=-1),!1)}advance(e){for(;;){if(this.stage==-1)return-1;if(this.stage==0){if(!this.nextStage(Ke(e,this.pos,this.start,!0)))return-1;if(e.charCodeAt(this.pos)!=58)return this.stage=-1;this.elts.push(g(u.LinkMark,this.pos+this.start,this.pos+this.start+1)),this.pos++}else if(this.stage==1){if(!this.nextStage(Ge(e,R(e,this.pos),this.start)))return-1}else if(this.stage==2){let r=R(e,this.pos),n=0;if(r>this.pos){let s=Ve(e,r,this.start);if(s){let i=W(e,s.to-this.start);i>0&&(this.nextStage(s),n=i)}}return n||(n=W(e,this.pos)),n>0&&n<e.length?n:-1}else return W(e,this.pos)}}};function W(t,e){for(;e<t.length;e++){let r=t.charCodeAt(e);if(r==10)break;if(!A(r))return-1}return e}var oe=class{nextLine(e,r,n){let s=r.depth<e.stack.length?-1:$e(r),i=r.next;if(s<0)return!1;let o=g(u.HeaderMark,e.lineStart+r.pos,e.lineStart+s);return e.nextLine(),e.addLeafElement(n,g(i==61?u.SetextHeading1:u.SetextHeading2,n.start,e.prevLineEnd(),[...e.parser.parseInline(n.content,n.start),o])),!0}finish(){return!1}},qe={LinkReference(t,e){return e.content.charCodeAt(0)==91?new ie(e):null},SetextHeading(){return new oe}},Ct=[(t,e)=>Xe(e)>=0,(t,e)=>Te(e)>=0,(t,e)=>De(e)>=0,(t,e)=>pe(e,t,!0)>=0,(t,e)=>ce(e,t,!0)>=0,(t,e)=>de(e,t,!0)>=0,(t,e)=>_e(e,t,!0)>=0],At={text:"",end:0},ae=class{constructor(e,r,n,s){this.parser=e,this.input=r,this.ranges=s,this.line=new re,this.atEnd=!1,this.reusePlaceholders=new Map,this.stoppedAt=null,this.rangeI=0,this.to=s[s.length-1].to,this.lineStart=this.absoluteLineStart=this.absoluteLineEnd=s[0].from,this.block=Q.create(u.Document,0,this.lineStart,0,0),this.stack=[this.block],this.fragments=n.length?new fe(n,r):null,this.readLine()}get parsedPos(){return this.absoluteLineStart}advance(){if(this.stoppedAt!=null&&this.absoluteLineStart>this.stoppedAt)return this.finish();let{line:e}=this;for(;;){for(let n=0;;){let s=e.depth<this.stack.length?this.stack[this.stack.length-1]:null;for(;n<e.markers.length&&(!s||e.markers[n].from<s.end);){let i=e.markers[n++];this.addNode(i.type,i.from,i.to)}if(!s)break;this.finishContext()}if(e.pos<e.text.length)break;if(!this.nextLine())return this.finish()}if(this.fragments&&this.reuseFragment(e.basePos))return null;e:for(;;){for(let n of this.parser.blockParsers)if(n){let s=n(this,e);if(s!=!1){if(s==!0)return null;e.forward();continue e}}break}if(e.pos==e.text.length)return this.nextLine()?null:this.finish();let r=new te(this.lineStart+e.pos,e.text.slice(e.pos));for(let n of this.parser.leafBlockParsers)if(n){let s=n(this,r);s&&r.parsers.push(s)}e:for(;this.nextLine()&&e.pos!=e.text.length;){if(e.indent<e.baseIndent+4){for(let n of this.parser.endLeafBlock)if(n(this,e,r))break e}for(let n of r.parsers)if(n.nextLine(this,e,r))return null;r.content+=`
|
||||
`+e.scrub();for(let n of e.markers)r.marks.push(n)}return this.finishLeaf(r),null}stopAt(e){if(this.stoppedAt!=null&&this.stoppedAt<e)throw new RangeError("Can't move stoppedAt forward");this.stoppedAt=e}reuseFragment(e){if(!this.fragments.moveTo(this.absoluteLineStart+e,this.absoluteLineStart)||!this.fragments.matches(this.block.hash))return!1;let r=this.fragments.takeNodes(this);return r?(this.absoluteLineStart+=r,this.lineStart=Ye(this.absoluteLineStart,this.ranges),this.moveRangeI(),this.absoluteLineStart<this.to?(this.lineStart++,this.absoluteLineStart++,this.readLine()):(this.atEnd=!0,this.readLine()),!0):!1}get depth(){return this.stack.length}parentType(e=this.depth-1){return this.parser.nodeSet.types[this.stack[e].type]}nextLine(){return this.lineStart+=this.line.text.length,this.absoluteLineEnd>=this.to?(this.absoluteLineStart=this.absoluteLineEnd,this.atEnd=!0,this.readLine(),!1):(this.lineStart++,this.absoluteLineStart=this.absoluteLineEnd+1,this.moveRangeI(),this.readLine(),!0)}peekLine(){return this.scanLine(this.absoluteLineEnd+1).text}moveRangeI(){for(;this.rangeI<this.ranges.length-1&&this.absoluteLineStart>=this.ranges[this.rangeI].to;)this.rangeI++,this.absoluteLineStart=Math.max(this.absoluteLineStart,this.ranges[this.rangeI].from)}scanLine(e){let r=At;if(r.end=e,e>=this.to)r.text="";else if(r.text=this.lineChunkAt(e),r.end+=r.text.length,this.ranges.length>1){let n=this.absoluteLineStart,s=this.rangeI;for(;this.ranges[s].to<r.end;){s++;let i=this.ranges[s].from,o=this.lineChunkAt(i);r.end=i+o.length,r.text=r.text.slice(0,this.ranges[s-1].to-n)+o,n=r.end-r.text.length}}return r}readLine(){let{line:e}=this,{text:r,end:n}=this.scanLine(this.absoluteLineStart);for(this.absoluteLineEnd=n,e.reset(r);e.depth<this.stack.length;e.depth++){let s=this.stack[e.depth],i=this.parser.skipContextMarkup[s.type];if(!i)throw new Error("Unhandled block context "+u[s.type]);let o=this.line.markers.length;if(!i(s,this,e)){this.line.markers.length>o&&(s.end=this.line.markers[this.line.markers.length-1].to),e.forward();break}e.forward()}}lineChunkAt(e){let r=this.input.chunk(e),n;if(this.input.lineChunks)n=r==`
|
||||
`?"":r;else{let s=r.indexOf(`
|
||||
`);n=s<0?r:r.slice(0,s)}return e+n.length>this.to?n.slice(0,this.to-e):n}prevLineEnd(){return this.atEnd?this.lineStart:this.lineStart-1}startContext(e,r,n=0){this.block=Q.create(e,n,this.lineStart+r,this.block.hash,this.lineStart+this.line.text.length),this.stack.push(this.block)}startComposite(e,r,n=0){this.startContext(this.parser.getNodeType(e),r,n)}addNode(e,r,n){typeof e=="number"&&(e=new x(this.parser.nodeSet.types[e],y,y,(n??this.prevLineEnd())-r)),this.block.addChild(e,r-this.block.from)}addElement(e){this.block.addChild(e.toTree(this.parser.nodeSet),e.from-this.block.from)}addLeafElement(e,r){this.addNode(this.buffer.writeElements(he(r.children,e.marks),-r.from).finish(r.type,r.to-r.from),r.from)}finishContext(){let e=this.stack.pop(),r=this.stack[this.stack.length-1];r.addChild(e.toTree(this.parser.nodeSet),e.from-r.from),this.block=r}finish(){for(;this.stack.length>1;)this.finishContext();return this.addGaps(this.block.toTree(this.parser.nodeSet,this.lineStart))}addGaps(e){return this.ranges.length>1?Qe(this.ranges,0,e.topNode,this.ranges[0].from,this.reusePlaceholders):e}finishLeaf(e){for(let n of e.parsers)if(n.finish(this,e))return;let r=he(this.parser.parseInline(e.content,e.start),e.marks);this.addNode(this.buffer.writeElements(r,-e.start).finish(u.Paragraph,e.content.length),e.start)}elt(e,r,n,s){return typeof e=="string"?g(this.parser.getNodeType(e),r,n,s):new G(e,r)}get buffer(){return new Z(this.parser.nodeSet)}};function Qe(t,e,r,n,s){let i=t[e].to,o=[],a=[],l=r.from+n;function h(f,p){for(;p?f>=i:f>i;){let d=t[e+1].from-i;n+=d,f+=d,e++,i=t[e].to}}for(let f=r.firstChild;f;f=f.nextSibling){h(f.from+n,!0);let p=f.from+n,d,c=s.get(f.tree);c?d=c:f.to+n>i?(d=Qe(t,e,f,n,s),h(f.to+n,!1)):d=f.toTree(),o.push(d),a.push(p-l)}return h(r.to+n,!1),new x(r.type,o,a,r.to+n-l,r.tree?r.tree.propValues:void 0)}var F=class t extends bt{constructor(e,r,n,s,i,o,a,l,h){super(),this.nodeSet=e,this.blockParsers=r,this.leafBlockParsers=n,this.blockNames=s,this.endLeafBlock=i,this.skipContextMarkup=o,this.inlineParsers=a,this.inlineNames=l,this.wrappers=h,this.nodeTypes=Object.create(null);for(let f of e.types)this.nodeTypes[f.name]=f.id}createParse(e,r,n){let s=new ae(this,e,r,n);for(let i of this.wrappers)s=i(s,e,r,n);return s}configure(e){let r=le(e);if(!r)return this;let{nodeSet:n,skipContextMarkup:s}=this,i=this.blockParsers.slice(),o=this.leafBlockParsers.slice(),a=this.blockNames.slice(),l=this.inlineParsers.slice(),h=this.inlineNames.slice(),f=this.endLeafBlock.slice(),p=this.wrappers;if(z(r.defineNodes)){s=Object.assign({},s);let d=n.types.slice(),c;for(let L of r.defineNodes){let{name:k,block:E,composite:w,style:S}=typeof L=="string"?{name:L}:L;if(d.some(M=>M.name==k))continue;w&&(s[d.length]=(M,O,Lt)=>w(O,Lt,M.value));let b=d.length,B=w?["Block","BlockContext"]:E?b>=u.ATXHeading1&&b<=u.SetextHeading2?["Block","LeafBlock","Heading"]:["Block","LeafBlock"]:void 0;d.push(J.define({id:b,name:k,props:B&&[[D.group,B]]})),S&&(c||(c={}),Array.isArray(S)||S instanceof St?c[k]=S:Object.assign(c,S))}n=new Oe(d),c&&(n=n.extend(ze(c)))}if(z(r.props)&&(n=n.extend(...r.props)),z(r.remove))for(let d of r.remove){let c=this.blockNames.indexOf(d),L=this.inlineNames.indexOf(d);c>-1&&(i[c]=o[c]=void 0),L>-1&&(l[L]=void 0)}if(z(r.parseBlock))for(let d of r.parseBlock){let c=a.indexOf(d.name);if(c>-1)i[c]=d.parse,o[c]=d.leaf;else{let L=d.before?q(a,d.before):d.after?q(a,d.after)+1:a.length-1;i.splice(L,0,d.parse),o.splice(L,0,d.leaf),a.splice(L,0,d.name)}d.endLeaf&&f.push(d.endLeaf)}if(z(r.parseInline))for(let d of r.parseInline){let c=h.indexOf(d.name);if(c>-1)l[c]=d.parse;else{let L=d.before?q(h,d.before):d.after?q(h,d.after)+1:h.length-1;l.splice(L,0,d.parse),h.splice(L,0,d.name)}}return r.wrap&&(p=p.concat(r.wrap)),new t(n,i,o,a,f,s,l,h,p)}getNodeType(e){let r=this.nodeTypes[e];if(r==null)throw new RangeError(`Unknown node type '${e}'`);return r}parseInline(e,r){let n=new P(this,e,r);e:for(let s=r;s<n.end;){let i=n.char(s);for(let o of this.inlineParsers)if(o){let a=o(n,i,s);if(a>=0){s=a;continue e}}s++}return n.resolveMarkers(0)}};function z(t){return t!=null&&t.length>0}function le(t){if(!Array.isArray(t))return t;if(t.length==0)return null;let e=le(t[0]);if(t.length==1)return e;let r=le(t.slice(1));if(!r||!e)return e||r;let n=(o,a)=>(o||y).concat(a||y),s=e.wrap,i=r.wrap;return{props:n(e.props,r.props),defineNodes:n(e.defineNodes,r.defineNodes),parseBlock:n(e.parseBlock,r.parseBlock),parseInline:n(e.parseInline,r.parseInline),remove:n(e.remove,r.remove),wrap:s?i?(o,a,l,h)=>s(i(o,a,l,h),a,l,h):s:i}}function q(t,e){let r=t.indexOf(e);if(r<0)throw new RangeError(`Position specified relative to unknown parser ${e}`);return r}var Ze=[J.none];for(let t=1,e;e=u[t];t++)Ze[t]=J.define({id:t,name:e,props:t>=u.Escape?[]:[[D.group,t in Re?["Block","BlockContext"]:["Block","LeafBlock"]]],top:e=="Document"});var y=[],Z=class{constructor(e){this.nodeSet=e,this.content=[],this.nodes=[]}write(e,r,n,s=0){return this.content.push(e,r,n
Line truncated
|
||||
`;)i--;this.fragmentEnd=i?i-1:0}let n=this.cursor;n||(n=this.cursor=this.fragment.tree.cursor(Ce.IncludeAnonymous|Ce.IgnoreMounts),n.firstChild());let s=e+this.fragment.offset;for(;n.to<=s;)if(!n.parent())return!1;for(;;){if(n.from>=s)return this.fragment.from<=r;if(!n.childAfter(s))return!1}}matches(e){let r=this.cursor.tree;for(;r&&r.type.isAnonymous&&r.children[0]instanceof x;)r=r.children[0];return r&&r.prop(D.contextHash)==e}takeNodes(e){let r=this.cursor,n=this.fragment.offset,s=this.fragmentEnd-(this.fragment.openEnd?1:0),i=e.absoluteLineStart,o=i,a=e.block.children.length,l=e.lineStart-e.block.from>>2,h=o,f=a;e:for(;;){if(r.to-n>Math.min(s,e.to)||r.type.isAnonymous&&r.tree&&(r.to-r.from>l||!It(r.tree))){if(r.type.isAnonymous&&r.firstChild())continue;break}let p=Ye(r.from-n,e.ranges);if(r.to-n<=e.ranges[e.rangeI].to)e.addNode(r.tree,p);else{let d=new x(e.parser.nodeSet.types[u.Paragraph],[],[],0,e.block.hashProp);e.reusePlaceholders.set(d,r.tree),e.addNode(d,p)}for((r.type.isAnonymous||r.type.is("Block"))&&(r.type.isAnonymous||Je.indexOf(r.type.id)<0?(o=r.to-n,a=e.block.children.length):(o=h,a=f),h=r.to-n,f=e.block.children.length);;){if(r.nextSibling())continue e;if(!r.parent()||!r.type.isAnonymous)break e}}for(;e.block.children.length>a;)e.block.children.pop(),e.block.positions.pop();return o-i}};function It(t){for(;t.type.isAnonymous;){let e=t.children[t.children.length-1];if(e instanceof x)t=e;else return!1}return t.type.is("Block")&&Je.indexOf(t.type.id)<0}function Ye(t,e){let r=t;for(let n=1;n<e.length;n++){let s=e[n-1].to,i=e[n].from;s<t&&(r-=i-s)}return r}var Et=ze({"Blockquote/...":m.quote,HorizontalRule:m.contentSeparator,"ATXHeading1/... SetextHeading1/...":m.heading1,"ATXHeading2/... SetextHeading2/...":m.heading2,"ATXHeading3/...":m.heading3,"ATXHeading4/...":m.heading4,"ATXHeading5/...":m.heading5,"ATXHeading6/...":m.heading6,"Comment CommentBlock":m.comment,Escape:m.escape,Entity:m.character,"Emphasis/...":m.emphasis,"StrongEmphasis/...":m.strong,"Link/... Image/...":m.link,"OrderedList/... BulletList/...":m.list,"BlockQuote/...":m.quote,"InlineCode CodeText":m.monospace,"URL Autolink":m.url,"HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark":m.processingInstruction,"CodeInfo LinkLabel":m.labelName,LinkTitle:m.string,Paragraph:m.content}),We=new F(new Oe(Ze).extend(Et),Object.keys(_).map(t=>_[t]),Object.keys(_).map(t=>qe[t]),Object.keys(_),Ct,Re,Object.keys(ee).map(t=>ee[t]),Object.keys(ee),[]);function Bt(t,e,r){let n=[];for(let s=t.firstChild,i=e;;s=s.nextSibling){let o=s?s.from:r;if(o>i&&n.push({from:i,to:o}),!s)break;i=s.to}return n}function et(t){let{codeParser:e,htmlParser:r}=t;return{wrap:wt((s,i)=>{let o=s.type.id;if(e&&(o==u.CodeBlock||o==u.FencedCode)){let a="";if(o==u.FencedCode){let h=s.node.getChild(u.CodeInfo);h&&(a=i.read(h.from,h.to))}let l=e(a);if(l)return{parser:l,overlay:h=>h.type.id==u.CodeText,bracketed:o==u.FencedCode}}else if(r&&(o==u.HTMLBlock||o==u.HTMLTag||o==u.CommentBlock))return{parser:r,overlay:Bt(s.node,s.from,s.to)};return null})}}var Mt={resolve:"Strikethrough",mark:"StrikethroughMark"},Ht={defineNodes:[{name:"Strikethrough",style:{"Strikethrough/...":m.strikethrough}},{name:"StrikethroughMark",style:m.processingInstruction}],parseInline:[{name:"Strikethrough",parse(t,e,r){if(e!=126||t.char(r+1)!=126||t.char(r+2)==126)return-1;let n=t.slice(r-1,r),s=t.slice(r+2,r+3),i=/\s|^$/.test(n),o=/\s|^$/.test(s),a=X.test(n),l=X.test(s);return t.addDelimiter(Mt,r,r+2,!o&&(!l||i||a),!i&&(!a||o||l))},after:"Emphasis"}]};function T(t,e,r=0,n,s=0){let i=0,o=!0,a=-1,l=-1,h=!1,f=()=>{n.push(t.elt("TableCell",s+a,s+l,t.parser.parseInline(e.slice(a,l),s+a)))};for(let p=r;p<e.length;p++){let d=e.charCodeAt(p);d==124&&!h?((!o||a>-1)&&i++,o=!1,n&&(a>-1&&f(),n.push(t.elt("TableDelimiter",p+s,p+s+1))),a=l=-1):(h||d!=32&&d!=9)&&(a<0&&(a=p),l=p+1),h=!h&&d==92}return a>-1&&(i++,n&&f()),i}function Be(t,e){for(let r=e;r<t.length;r++){let n=t.charCodeAt(r);if(n==124)return!0;n==92&&r++}return!1}var tt=/^[>\s]*\|?(\s*:?-+:?\s*\|)+(\s*:?-+:?\s*)?$/,K=class{constructor(){this.rows=null}nextLine(e,r,n){if(this.rows==null){this.rows=!1;let s;if((r.next==45||r.next==58||r.next==124)&&tt.test(s=r.text.slice(r.pos))){let i=[];T(e,n.content,0,i,n.start)==T(e,s,0)&&(this.rows=[e.elt("TableHeader",n.start,n.start+n.content.length,i),e.elt("TableDelimiter",e.lineStart+r.pos,e.lineStart+r.text.length)])}}else if(this.rows){let s=[];T(e,r.text,r.pos,s,e.lineStart),this.rows.push(e.elt("TableRow",e.lineStart+r.pos,e.lineStart+r.text.length,s))}return!1}finish(e,r){return this.rows?(e.addLeafElement(r,e.elt("Table",r.start,r.start+r.content.length,this.rows)),!0):!1}},vt={defineNodes:[{name:"Table",block:!0},{name:"TableHeader",style:{"TableHeader/...":m.heading}},"TableRow",{name:"TableCell",style:m.content},{name:"TableDelimiter",style:m.processingInstruction}],parseBlock:[{name:"Table",leaf(t,e){return Be(e.content,0)?new K:null},endLeaf(t,e,r){if(r.parsers.some(s=>s instanc
Line truncated
|
||||
//# sourceMappingURL=lang-markdown.bundle.mjs.map
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/* esm.sh - @codemirror/lang-python@6.2.1 */
|
||||
import{ExternalTokenizer as $,ContextTracker as L,LRParser as H}from"./lezer-lr.mjs";import{styleTags as B,tags as n}from"./lezer-highlight.mjs";var K=1,G=194,w=195,M=196,x=197,OO=198,eO=199,iO=200,aO=2,Z=3,u=201,nO=24,rO=25,QO=49,tO=50,oO=55,dO=56,sO=57,lO=59,TO=60,SO=61,pO=62,qO=63,gO=65,mO=238,PO=71,$O=241,hO=242,cO=243,fO=244,XO=245,yO=246,zO=247,WO=248,j=72,vO=249,RO=250,kO=251,_O=252,xO=253,uO=254,UO=255,bO=256,VO=73,GO=77,wO=263,ZO=112,jO=130,EO=151,YO=152,FO=155,p=10,q=13,z=32,h=9,W=35,CO=40,JO=46,y=123,U=125,E=39,Y=34,b=92,AO=111,NO=120,IO=78,DO=117,LO=85,HO=new Set([rO,QO,tO,wO,gO,jO,dO,sO,mO,pO,qO,j,VO,GO,TO,SO,EO,YO,FO,ZO]);function f(O){return O==p||O==q}function X(O){return O>=48&&O<=57||O>=65&&O<=70||O>=97&&O<=102}var BO=new $((O,e)=>{let i;if(O.next<0)O.acceptToken(eO);else if(e.context.flags&m)f(O.next)&&O.acceptToken(OO,1);else if(((i=O.peek(-1))<0||f(i))&&e.canShift(x)){let a=0;for(;O.next==z||O.next==h;)O.advance(),a++;(O.next==p||O.next==q||O.next==W)&&O.acceptToken(x,-a)}else f(O.next)&&O.acceptToken(M,1)},{contextual:!0}),KO=new $((O,e)=>{let i=e.context;if(i.flags)return;let a=O.peek(-1);if(a==p||a==q){let r=0,Q=0;for(;;){if(O.next==z)r++;else if(O.next==h)r+=8-r%8;else break;O.advance(),Q++}r!=i.indent&&O.next!=p&&O.next!=q&&O.next!=W&&(r<i.indent?O.acceptToken(w,-Q):O.acceptToken(G))}}),m=1,F=2,d=4,s=8,l=16,T=32;function P(O,e,i){this.parent=O,this.indent=e,this.flags=i,this.hash=(O?O.hash+O.hash<<8:0)+e+(e<<4)+i+(i<<6)}var MO=new P(null,0,0);function Oe(O){let e=0;for(let i=0;i<O.length;i++)e+=O.charCodeAt(i)==h?8-e%8:1;return e}var V=new Map([[$O,0],[hO,d],[cO,s],[fO,s|d],[XO,l],[yO,l|d],[zO,l|s],[WO,l|s|d],[vO,T],[RO,T|d],[kO,T|s],[_O,T|s|d],[xO,T|l],[uO,T|l|d],[UO,T|l|s],[bO,T|l|s|d]].map(([O,e])=>[O,e|F])),ee=new L({start:MO,reduce(O,e,i,a){return O.flags&m&&HO.has(e)||(e==PO||e==j)&&O.flags&F?O.parent:O},shift(O,e,i,a){return e==G?new P(O,Oe(a.read(a.pos,i.pos)),0):e==w?O.parent:e==nO||e==oO||e==lO||e==Z?new P(O,0,m):V.has(e)?new P(O,0,V.get(e)|O.flags&m):O},hash(O){return O.hash}}),ie=new $(O=>{for(let e=0;e<5;e++){if(O.next!="print".charCodeAt(e))return;O.advance()}if(!/\w/.test(String.fromCharCode(O.next)))for(let e=0;;e++){let i=O.peek(e);if(!(i==z||i==h)){i!=CO&&i!=JO&&i!=p&&i!=q&&i!=W&&O.acceptToken(K);return}}}),ae=new $((O,e)=>{let{flags:i}=e.context,a=i&d?Y:E,r=(i&s)>0,Q=!(i&l),t=(i&T)>0,o=O.pos;for(;!(O.next<0);)if(t&&O.next==y)if(O.peek(1)==y)O.advance(2);else{if(O.pos==o){O.acceptToken(Z,1);return}break}else if(Q&&O.next==b){if(O.pos==o){O.advance();let g=O.next;g>=0&&(O.advance(),ne(O,g)),O.acceptToken(aO);return}break}else if(O.next==b&&!Q&&O.peek(1)>-1)O.advance(2);else if(O.next==a&&(!r||O.peek(1)==a&&O.peek(2)==a)){if(O.pos==o){O.acceptToken(u,r?3:1);return}break}else if(O.next==p){if(r)O.advance();else if(O.pos==o){O.acceptToken(u);return}break}else O.advance();O.pos>o&&O.acceptToken(iO)});function ne(O,e){if(e==AO)for(let i=0;i<2&&O.next>=48&&O.next<=55;i++)O.advance();else if(e==NO)for(let i=0;i<2&&X(O.next);i++)O.advance();else if(e==DO)for(let i=0;i<4&&X(O.next);i++)O.advance();else if(e==LO)for(let i=0;i<8&&X(O.next);i++)O.advance();else if(e==IO&&O.next==y){for(O.advance();O.next>=0&&O.next!=U&&O.next!=E&&O.next!=Y&&O.next!=p;)O.advance();O.next==U&&O.advance()}}var re=B({'async "*" "**" FormatConversion FormatSpec':n.modifier,"for while if elif else try except finally return raise break continue with pass assert await yield match case":n.controlKeyword,"in not and or is del":n.operatorKeyword,"from def class global nonlocal lambda":n.definitionKeyword,import:n.moduleKeyword,"with as print":n.keyword,Boolean:n.bool,None:n.null,VariableName:n.variableName,"CallExpression/VariableName":n.function(n.variableName),"FunctionDefinition/VariableName":n.function(n.definition(n.variableName)),"ClassDefinition/VariableName":n.definition(n.className),PropertyName:n.propertyName,"CallExpression/MemberExpression/PropertyName":n.function(n.propertyName),Comment:n.lineComment,Number:n.number,String:n.string,FormatString:n.special(n.string),Escape:n.escape,UpdateOp:n.updateOperator,"ArithOp!":n.arithmeticOperator,BitOp:n.bitwiseOperator,CompareOp:n.compareOperator,AssignOp:n.definitionOperator,Ellipsis:n.punctuation,At:n.meta,"( )":n.paren,"[ ]":n.squareBracket,"{ }":n.brace,".":n.derefOperator,", ;":n.separator}),Qe={__proto__:null,await:44,or:54,and:56,in:60,not:62,is:64,if:70,else:72,lambda:76,yield:94,from:96,async:102,for:104,None:162,True:164,False:164,del:178,pass:182,break:186,continue:190,return:194,raise:202,import:206,as:208,global:212,nonlocal:214,assert:218,type:223,elif:236,while:240,try:246,except:248,finally:250,with:254,def:258,class:268,match:279,case:285},C=H.deserialize({version:14,states:"##jQ`QeOOP$}OSOOO&WQtO'#HUOOQS'#Co'#CoOOQS'#Cp'#CpO'vQdO'#CnO*UQtO'#HTOOQS'#HU'#HUOOQS'#DU'#DUOOQS'#HT'#HTO*rQdO'#D_O+VQdO'#DfO+gQdO'#DjO+zOWO'#DuO,VOWO'#DvO.[QtO'#GuOOQS'#Gu'#GuO'vQdO'#GtO0ZQtO'#GtOOQS'#Eb'#EbO0rQdO'#EcOOQS'#Gs'#GsO0|QdO'#GrOOQV'#Gr'#GrO1XQdO'#
Line truncated
|
||||
|
||||
`,{label:"if",detail:"block",type:"keyword"}),S("if ${}:\n ${}\nelse:\n ${}",{label:"if",detail:"/ else block",type:"keyword"}),S("class ${name}:\n def __init__(self, ${params}):\n ${}",{label:"class",detail:"definition",type:"keyword"}),S("import ${module}",{label:"import",detail:"statement",type:"keyword"}),S("from ${module} import ${names}",{label:"from",detail:"import",type:"keyword"})],ce=qe(D,ge($e.concat(he)));function R(O){let{node:e,pos:i}=O,a=O.lineIndent(i,-1),r=null;for(;;){let Q=e.childBefore(i);if(Q)if(Q.name=="Comment")i=Q.from;else if(Q.name=="Body"||Q.name=="MatchBody")O.baseIndentFor(Q)+O.unit<=a&&(r=Q),e=Q;else if(Q.name=="MatchClause")e=Q;else if(Q.type.is("Statement"))e=Q;else break;else break}return r}function k(O,e){let i=O.baseIndentFor(e),a=O.lineAt(O.pos,-1),r=a.from+a.text.length;return/^\s*($|#)/.test(a.text)&&O.node.to<r+100&&!/\S/.test(O.state.sliceDoc(r,O.node.to))&&O.lineIndent(O.pos,-1)<=i||/^\s*(else:|elif |except |finally:|case\s+[^=:]+:)/.test(O.textAfter)&&O.lineIndent(O.pos,-1)>i?null:i+O.unit}var _=oe.define({name:"python",parser:C.configure({props:[de.add({Body:O=>{var e;let i=/^\s*(#|$)/.test(O.textAfter)&&R(O)||O.node;return(e=k(O,i))!==null&&e!==void 0?e:O.continue()},MatchBody:O=>{var e;let i=R(O);return(e=k(O,i||O.node))!==null&&e!==void 0?e:O.continue()},IfStatement:O=>/^\s*(else:|elif )/.test(O.textAfter)?O.baseIndent:O.continue(),"ForStatement WhileStatement":O=>/^\s*else:/.test(O.textAfter)?O.baseIndent:O.continue(),TryStatement:O=>/^\s*(except[ :]|finally:|else:)/.test(O.textAfter)?O.baseIndent:O.continue(),MatchStatement:O=>/^\s*case /.test(O.textAfter)?O.baseIndent+O.unit:O.continue(),"TupleExpression ComprehensionExpression ParamList ArgList ParenthesizedExpression":v({closing:")"}),"DictionaryExpression DictionaryComprehensionExpression SetExpression SetComprehensionExpression":v({closing:"}"}),"ArrayExpression ArrayComprehensionExpression":v({closing:"]"}),MemberExpression:O=>O.baseIndent+O.unit,"String FormatString":()=>null,Script:O=>{var e;let i=R(O);return(e=i&&k(O,i))!==null&&e!==void 0?e:O.continue()}}),se.add({"ArrayExpression DictionaryExpression SetExpression TupleExpression":le,Body:(O,e)=>({from:O.from+1,to:O.to-(O.to==e.doc.length?0:1)}),"String FormatString":(O,e)=>({from:e.doc.lineAt(O.from).to,to:O.to})})]}),languageData:{closeBrackets:{brackets:["(","[","{","'",'"',"'''",'"""'],stringPrefixes:["f","fr","rf","r","u","b","br","rb","F","FR","RF","R","U","B","BR","RB"]},commentTokens:{line:"#"},indentOnInput:/^\s*([\}\]\)]|else:|elif |except |finally:|case\s+[^:]*:?)$/}});function ke(){return new Te(_,[_.data.of({autocomplete:Pe}),_.data.of({autocomplete:ce})])}export{ce as globalCompletion,Pe as localCompletionSource,ke as python,_ as pythonLanguage};
|
||||
//# sourceMappingURL=lang-python.bundle.mjs.map
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/* esm.sh - @codemirror/language@6.13.1 */
|
||||
import{NodeProp as v,Tree as b,TreeFragment as $,Parser as Fe,IterMode as Te,NodeType as Z,NodeSet as ut}from"./lezer-common.mjs";import{StateEffect as Q,StateField as Ue,Facet as T,EditorState as M,countColumn as ct,combineConfig as We,RangeSetBuilder as de,RangeSet as je,Prec as Ve}from"./cm-state.mjs";import{ViewPlugin as j,logException as dt,EditorView as S,Decoration as g,WidgetType as He,gutter as pt,GutterMarker as mt,Direction as _}from"./cm-view.mjs";import{tags as u,highlightTree as gt,tagHighlighter as kt,styleTags as bt}from"./lezer-highlight.mjs";var ye=typeof Symbol>"u"?"__\u037C":Symbol.for("\u037C"),Y=typeof Symbol>"u"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),we=typeof globalThis<"u"?globalThis:typeof window<"u"?window:{},O=class{constructor(e,t){this.rules=[];let{finish:r}=t||{};function i(o){return/^@/.test(o)?[o]:o.split(/,\s*/)}function s(o,a,l,h){let f=[],c=/^@(\w+)\b/.exec(o[0]),m=c&&c[1]=="keyframes";if(c&&a==null)return l.push(o[0]+";");for(let d in a){let k=a[d];if(/&/.test(d))s(d.split(/,\s*/).map(w=>o.map(I=>w.replace(/&/,I))).reduce((w,I)=>w.concat(I)),k,l);else if(k&&typeof k=="object"){if(!c)throw new RangeError("The value of a property ("+d+") should be a primitive value.");s(i(d),k,f,m)}else k!=null&&f.push(d.replace(/_.*/,"").replace(/[A-Z]/g,w=>"-"+w.toLowerCase())+": "+k+";")}(f.length||m)&&l.push((r&&!c&&!h?o.map(r):o).join(", ")+" {"+f.join(" ")+"}")}for(let o in e)s(i(o),e[o],this.rules)}getRules(){return this.rules.join(`
|
||||
`)}static newName(){let e=we[ye]||1;return we[ye]=e+1,"\u037C"+e.toString(36)}static mount(e,t,r){let i=e[Y],s=r&&r.nonce;i?s&&i.setNonce(s):i=new ee(e,s),i.mount(Array.isArray(t)?t:[t],e)}},Se=new Map,ee=class{constructor(e,t){let r=e.ownerDocument||e,i=r.defaultView;if(!e.head&&e.adoptedStyleSheets&&i.CSSStyleSheet){let s=Se.get(r);if(s)return e[Y]=s;this.sheet=new i.CSSStyleSheet,Se.set(r,this)}else this.styleTag=r.createElement("style"),t&&this.styleTag.setAttribute("nonce",t);this.modules=[],e[Y]=this}mount(e,t){let r=this.sheet,i=0,s=0,o=!1;for(let a=0;a<e.length;a++){let l=e[a],h=this.modules.indexOf(l);if(h<s&&h>-1&&(this.modules.splice(h,1),o=!0,s--,h=-1),h==-1){if(this.modules.splice(s++,0,l),o=!0,r)for(let f=0;f<l.rules.length;f++)r.insertRule(l.rules[f],i++)}else{for(;s<h;)i+=this.modules[s++].rules.length;i+=l.rules.length,s++}}if(r)t.adoptedStyleSheets.indexOf(this.sheet)<0&&(t.adoptedStyleSheets=[this.sheet,...t.adoptedStyleSheets]);else{if(o){let l="";for(let h=0;h<this.modules.length;h++)l+=this.modules[h].getRules()+`
|
||||
`;this.styleTag.textContent=l}let a=t.head||t;this.styleTag.parentNode!=a&&a.insertBefore(this.styleTag,a.firstChild)}}setNonce(e){this.styleTag&&this.styleTag.getAttribute("nonce")!=e&&this.styleTag.setAttribute("nonce",e)}};function ve(n,e,t,r=0,i=0){e==null&&(e=n.search(/[^\s\u00a0]/),e==-1&&(e=n.length));let s=i;for(let o=r;o<e;o++)n.charCodeAt(o)==9?s+=t-s%t:s++;return s}var P=class{constructor(e,t,r,i){this.string=e,this.tabSize=t,this.indentUnit=r,this.overrideIndent=i,this.pos=0,this.start=0,this.lastColumnPos=0,this.lastColumnValue=0}eol(){return this.pos>=this.string.length}sol(){return this.pos==0}peek(){return this.string.charAt(this.pos)||void 0}next(){if(this.pos<this.string.length)return this.string.charAt(this.pos++)}eat(e){let t=this.string.charAt(this.pos),r;if(typeof e=="string"?r=t==e:r=t&&(e instanceof RegExp?e.test(t):e(t)),r)return++this.pos,t}eatWhile(e){let t=this.pos;for(;this.eat(e););return this.pos>t}eatSpace(){let e=this.pos;for(;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e}skipToEnd(){this.pos=this.string.length}skipTo(e){let t=this.string.indexOf(e,this.pos);if(t>-1)return this.pos=t,!0}backUp(e){this.pos-=e}column(){return this.lastColumnPos<this.start&&(this.lastColumnValue=ve(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue}indentation(){var e;return(e=this.overrideIndent)!==null&&e!==void 0?e:ve(this.string,null,this.tabSize)}match(e,t,r){if(typeof e=="string"){let i=o=>r?o.toLowerCase():o,s=this.string.substr(this.pos,e.length);return i(s)==i(e)?(t!==!1&&(this.pos+=e.length),!0):null}else{let i=this.string.slice(this.pos).match(e);return i&&i.index>0?null:(i&&t!==!1&&(this.pos+=i[0].length),i)}}current(){return this.string.slice(this.start,this.pos)}};var te,A=new v;function $e(n){return T.define({combine:n?e=>e.concat(n):void 0})}var yt=new v,p=class{constructor(e,t,r=[],i=""){this.data=e,this.name=i,M.prototype.hasOwnProperty("tree")||Object.defineProperty(M.prototype,"tree",{get(){return y(this)}}),this.parser=t,this.extension=[x.of(this),M.languageData.of((s,o,a)=>{let l=xe(s,o,a),h=l.type.prop(A);if(!h)return[];let f=s.facet(h),c=l.type.prop(yt);if(c){let m=l.resolve(o-l.from,a);for(let d of c)if(d.test(m,s)){let k=s.facet(d.facet);return d.type=="replace"?k:k.concat(f)}}return f})].concat(r)}isActiveAt(e,t,r=-1){return xe(e,t,r).type.prop(A)==this.data}findRegions(e){let t=e.facet(x);if(t?.data==this.data)return[{from:0,to:e.doc.length}];if(!t||!t.allowsNesting)return[];let r=[],i=(s,o)=>{if(s.prop(A)==this.data){r.push({from:o,to:o+s.length});return}let a=s.prop(v.mounted);if(a){if(a.tree.prop(A)==this.data){if(a.overlay)for(let l of a.overlay)r.push({from:l.from+o,to:l.to+o});else r.push({from:o,to:o+s.length});return}else if(a.overlay){let l=r.length;if(i(a.tree,a.overlay[0].from+o),r.length>l)return}}for(let l=0;l<s.children.length;l++){let h=s.children[l];h instanceof b&&i(h,s.positions[l]+o)}};return i(y(e),0),r}get allowsNesting(){return!0}};p.setState=Q.define();function xe(n,e,t){let r=n.facet(x),i=y(n).topNode;if(!r||r.allowsNesting)for(let s=i;s;s=s.enter(e,t,Te.ExcludeBuffers|Te.EnterBracketed))s.type.isTop&&(i=s);return i}var Ce=class n extends p{constructor(e,t,r){super(e,t,[],r),this.parser=t}static define(e){let t=$e(e.languageData);return new n(t,e.parser.configure({props:[A.add(r=>r.isTop?t:void 0)]}),e.name)}configure(e,t){return new n(this.data,this.parser.configure(e),t||this.name)}get allowsNesting(){return this.parser.hasWrappers()}};function y(n){let e=n.field(p.state,!1);return e?e.tree:b.empty}function wt(n,e,t=50){var r;let i=(r=n.field(p.state,!1))===null||r===void 0?void 0:r.context;if(!i)return null;let s=i.viewport;i.updateViewport({from:0,to:e});let o=i.isDone(e)||i.work(t,e)?i.tree:null;return i.updateViewport(s),o}function kn(n,e=n.doc.length){var t;return((t=n.field(p.state,!1))===null||t===void 0?void 0:t.context.isDone(e))||!1}function bn(n,e=n.viewport.to,t=100){let r=wt(n.state,e,t);return r!=y(n.state)&&n.dispatch({}),!!r}function yn(n){var e;return((e=n.plugin(_e))===null||e===void 0?void 0:e.isWorking())||!1}var ie=class{constructor(e){this.doc=e,this.cursorPos=0,this.string="",this.cursor=e.iter()}get length(){return this.doc.length}syncTo(e){return this.string=this.cursor.next(e-this.cursorPos).value,this.cursorPos=e+this.string.length,this.cursorPos-this.string.length}chunk(e){return this.syncTo(e),this.string}get lineChunks(){return!0}read(e,t){let r=this.cursorPos-this.string.length;return e<r||t>=this.cursorPos?this.doc.sliceString(e,t):this.string.slice(e-r,t-r)}},N=null,R=class n{constructor(e,t,r=[],i,s,o,a,l){this.parser=e,this.state=t,this.fragments=r,this.tree=i,this.treeLen=s,this.viewport=o,this.skipped=a,this.scheduleOn=l,this.parse=null,this.tempSkipped=[]}static create(e,t,r){return new n(e,t,[],b.empty,0,r,[],null)}startParse(){return this.parser.startParse(new ie(this.state.doc),this.fragments
Line truncated
|
||||
`&&(t="");else{let r=t.indexOf(`
|
||||
`);r>-1&&(t=t.slice(0,r))}return e+t.length<=this.to?t:t.slice(0,this.to-e)}nextLine(){let e=this.parsedPos,t=this.lineAfter(e),r=e+t.length;for(let i=this.rangeIndex;;){let s=this.ranges[i].to;if(s>=r||(t=t.slice(0,s-(r-t.length)),i++,i==this.ranges.length))break;let o=this.ranges[i].from,a=this.lineAfter(o);t+=a,r=o+a.length}return{line:t,end:r}}skipGapsTo(e,t,r){for(;;){let i=this.ranges[this.rangeIndex].to,s=e+t;if(r>0?i>s:i>=s)break;let o=this.ranges[++this.rangeIndex].from;t+=o-i}return t}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to<this.parsedPos;)this.rangeIndex++}emitToken(e,t,r,i){let s=4;if(this.ranges.length>1){i=this.skipGapsTo(t,i,1),t+=i;let a=this.chunk.length;i=this.skipGapsTo(r,i,-1),r+=i,s+=this.chunk.length-a}let o=this.chunk.length-4;return this.lang.streamParser.mergeTokens&&s==4&&o>=0&&this.chunk[o]==e&&this.chunk[o+2]==t?this.chunk[o+2]=r:this.chunk.push(e,t,r,s),i}parseLine(e){let{line:t,end:r}=this.nextLine(),i=0,{streamParser:s}=this.lang,o=new P(t,e?e.state.tabSize:4,e?q(e.state):2);if(o.eol())s.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let a=ot(s.token,o,this.state);if(a&&(i=this.emitToken(this.lang.tokenTable.resolve(a),this.parsedPos+o.start,this.parsedPos+o.pos,i)),o.start>1e4)break}this.parsedPos=r,this.moveRangeIndex(),this.parsedPos<this.to&&this.parsedPos++}finishChunk(){let e=b.build({buffer:this.chunk,start:this.chunkStart,length:this.parsedPos-this.chunkStart,nodeSet:tn,topID:0,maxBufferLength:512,reused:this.chunkReused});e=new b(e.type,e.children,e.positions,e.length,[[this.lang.stateAfter,this.lang.streamParser.copyState(this.state)]]),this.chunks.push(e),this.chunkPos.push(this.chunkStart-this.ranges[0].from),this.chunk=[],this.chunkReused=void 0,this.chunkStart=this.parsedPos}finish(){return new b(this.lang.topNode,this.chunks,this.chunkPos,this.parsedPos-this.ranges[0].from).balance()}};function ot(n,e,t){e.start=e.pos;for(let r=0;r<10;r++){let i=n(e,t);if(e.pos>e.start)return i}throw new Error("Stream parser failed to advance stream.")}var be=Object.create(null),W=[Z.none],tn=new ut(W),Re=[],Le=Object.create(null),lt=Object.create(null);for(let[n,e]of[["variable","variableName"],["variable-2","variableName.special"],["string-2","string.special"],["def","variableName.definition"],["tag","tagName"],["attribute","attributeName"],["type","typeName"],["builtin","variableName.standard"],["qualifier","modifier"],["error","invalid"],["header","heading"],["property","propertyName"]])lt[n]=at(be,e);var K=class{constructor(e){this.extra=e,this.table=Object.assign(Object.create(null),lt)}resolve(e){return e?this.table[e]||(this.table[e]=at(this.extra,e)):0}},nn=new K(be);function re(n,e){Re.indexOf(n)>-1||(Re.push(n),console.warn(e))}function at(n,e){let t=[];for(let a of e.split(" ")){let l=[];for(let h of a.split(".")){let f=n[h]||u[h];f?typeof f=="function"?l.length?l=l.map(f):re(h,`Modifier ${h} used at start of tag`):l.length?re(h,`Tag ${h} used as modifier`):l=Array.isArray(f)?f:[f]:re(h,`Unknown highlighting tag ${h}`)}for(let h of l)t.push(h)}if(!t.length)return 0;let r=e.replace(/ /g,"_"),i=r+" "+t.map(a=>a.id),s=Le[i];if(s)return s.id;let o=Le[i]=Z.define({id:W.length,name:r,props:[bt({[r]:t})]});return W.push(o),o.id}function rn(n,e){let t=Z.define({id:W.length,name:"Document",props:[A.add(()=>n),Ke.add(()=>r=>e.getIndent(r))],top:!0});return W.push(t),t}function ht(n){return n.length<=4096&&/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\ufb50-\ufdff]/.test(n)}function ft(n){for(let e=n.iter();!e.next().done;)if(ht(e.value))return!0;return!1}function sn(n){let e=!1;return n.iterChanges((t,r,i,s,o)=>{!e&&ft(o)&&(e=!0)}),e}var ce=T.define({combine:n=>n.some(e=>e)});function Ln(n={}){let e=[on];return n.alwaysIsolate&&e.push(ce.of(!0)),e}var on=j.fromClass(class{constructor(n){this.always=n.state.facet(ce)||n.textDirection!=_.LTR||n.state.facet(S.perLineTextDirection),this.hasRTL=!this.always&&ft(n.state.doc),this.tree=y(n.state),this.decorations=this.always||this.hasRTL?Ee(n,this.tree,this.always):g.none}update(n){let e=n.state.facet(ce)||n.view.textDirection!=_.LTR||n.state.facet(S.perLineTextDirection);if(!e&&!this.hasRTL&&sn(n.changes)&&(this.hasRTL=!0),!e&&!this.hasRTL)return;let t=y(n.state);(e!=this.always||t!=this.tree||n.docChanged||n.viewportChanged)&&(this.tree=t,this.always=e,this.decorations=Ee(n.view,t,e))}},{provide:n=>{function e(t){var r,i;return(i=(r=t.plugin(n))===null||r===void 0?void 0:r.decorations)!==null&&i!==void 0?i:g.none}return[S.outerDecorations.of(e),Ve.lowest(S.bidiIsolatedRanges.of(e))]}});function Ee(n,e,t){let r=new de,i=n.visibleRanges;t||(i=ln(i,n.state.doc));for(let{from:s,to:o}of i)e.iterate({enter:a=>{let l=a.type.prop(v.isolate);l&&r.add(a.from,a.to,an[l])},from:s,to:o});return r.finish()}function ln(n,e){let t=e.iter(),r=0,i=[],s=null;for(let{from:o,to:a}of n)if(!(s&&s.to>o&&(o=s.to,o>=a)))for(r+t.value.length<o&&(t.next(o-(r+t.value.length)),r=o);;){let l=r,h=r+t.value.length;if(!t.lineBreak&&ht(t.value)&&
Line truncated
|
||||
//# sourceMappingURL=language.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @codemirror/lint@6.9.7 */
|
||||
import{Decoration as T,showPanel as re,EditorView as E,ViewPlugin as ae,gutter as ce,showTooltip as fe,hoverTooltip as de,getPanel as ue,activateHover as U,logException as me,WidgetType as he,GutterMarker as ge}from"./cm-view.mjs";import{StateEffect as R,StateField as G,Facet as Z,combineConfig as X,RangeSet as j,RangeSetBuilder as pe}from"./cm-state.mjs";function k(){var t=arguments[0];typeof t=="string"&&(t=document.createElement(t));var e=1,n=arguments[1];if(n&&typeof n=="object"&&n.nodeType==null&&!Array.isArray(n)){for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)){var o=n[i];typeof o=="string"?t.setAttribute(i,o):o!=null&&(t[i]=o)}e++}for(;e<arguments.length;e++)_(t,arguments[e]);return t}function _(t,e){if(typeof e=="string")t.appendChild(document.createTextNode(e));else if(e!=null)if(e.nodeType!=null)t.appendChild(e);else if(Array.isArray(e))for(var n=0;n<e.length;n++)_(t,e[n]);else throw new RangeError("Unsupported child node: "+e)}var L=class{constructor(e,n,i){this.from=e,this.to=n,this.diagnostic=i}},C=class t{constructor(e,n,i){this.diagnostics=e,this.panel=n,this.selected=i}static init(e,n,i){let o=i.facet(y).markerFilter;o&&(e=o(e,i));let s=e.slice().sort((h,b)=>h.from-b.from||h.to-b.to),r=new pe,l=[],c=0,a=i.doc.iter(),f=0,u=i.doc.length;for(let h=0;;){let b=h==s.length?null:s[h];if(!b&&!l.length)break;let w,g;if(l.length)w=c,g=l.reduce((d,x)=>Math.min(d,x.to),b&&b.from>w?b.from:1e8);else{if(w=b.from,w>u)break;g=b.to,l.push(b),h++}for(;h<s.length;){let d=s[h];if(d.from==w&&(d.to>d.from||d.to==w))l.push(d),h++,g=Math.min(d.to,g);else{g=Math.min(d.from,g);break}}g=Math.min(g,u);let P=!1;if(l.some(d=>d.from==w&&(d.to==g||g==u))&&(P=w==g,!P&&g-w<10)){let d=w-(f+a.value.length);d>0&&(a.next(d),f=w);for(let x=w;;){if(x>=g){P=!0;break}if(!a.lineBreak&&f+a.value.length>x)break;x=f+a.value.length,f+=a.value.length,a.next()}}let N=oe(l);if(P)r.add(w,w,T.widget({widget:new B(N),diagnostics:l.slice()}));else{let d=l.reduce((x,V)=>V.markClass?x+" "+V.markClass:x,"");r.add(w,g,T.mark({class:"cm-lintRange cm-lintRange-"+N+d,diagnostics:l.slice(),inclusiveEnd:l.some(x=>x.to>g)}))}if(c=g,c==u)break;for(let d=0;d<l.length;d++)l[d].to<=c&&l.splice(d--,1)}let m=r.finish();return new t(m,n,v(m))}};function v(t,e=null,n=0){let i=null;return t.between(n,1e9,(o,s,{spec:r})=>{if(!(e&&r.diagnostics.indexOf(e)<0))if(!i)i=new L(o,s,e||r.diagnostics[0]);else{if(r.diagnostics.indexOf(i.diagnostic)<0)return!1;i=new L(i.from,s,i.diagnostic)}}),i}function W(t,e){let n=e.pos,i=e.end||n,o=t.state.facet(y).hideOn(t,n,i);if(o!=null)return o;let s=t.startState.doc.lineAt(e.pos);return!!(t.effects.some(r=>r.is(I))||t.changes.touchesRange(s.from,Math.max(s.to,i)))}function J(t,e){return t.field(p,!1)?e:e.concat(R.appendConfig.of(le))}function we(t,e){return{effects:J(t,[I.of(e)])}}var I=R.define(),z=R.define(),Q=R.define(),p=G.define({create(){return new C(T.none,null,null)},update(t,e){if(e.docChanged&&t.diagnostics.size){let n=t.diagnostics.map(e.changes),i=null,o=t.panel;if(t.selected){let s=e.changes.mapPos(t.selected.from,1);i=v(n,t.selected.diagnostic,s)||v(n,null,s)}!n.size&&o&&e.state.facet(y).autoPanel&&(o=null),t=new C(n,o,i)}for(let n of e.effects)if(n.is(I)){let i=e.state.facet(y).autoPanel?n.value.length?S.open:null:t.panel;t=C.init(n.value,i,e.state)}else n.is(z)?t=new C(t.diagnostics,n.value?S.open:null,t.selected):n.is(Q)&&(t=new C(t.diagnostics,t.panel,n.value));return t},provide:t=>[re.from(t,e=>e.panel),E.decorations.from(t,e=>e.diagnostics)]});function Ie(t){let e=t.field(p,!1);return e?e.diagnostics.size:0}var be=T.mark({class:"cm-lintRange cm-lintRange-active"});function ke(t,e,n){let{diagnostics:i}=t.state.field(p),o,s=-1,r=-1;i.between(e-(n<0?1:0),e+(n>0?1:0),(c,a,{spec:f})=>{if(e>=c&&e<=a&&(c==a||(e>c||n>0)&&(e<a||n<0)))return o=f.diagnostics,s=c,r=a,!1});let l=t.state.facet(y).tooltipFilter;return o&&l&&(o=l(o,t.state)),o?{pos:s,end:r,above:!0,create(){return{dom:ee(t,o)}}}:null}function ee(t,e){return k("ul",{class:"cm-tooltip-lint"},e.map(n=>ne(t,n,!1)))}var xe=t=>{let e=t.state.field(p,!1);(!e||!e.panel)&&t.dispatch({effects:J(t.state,[z.of(!0)])});let n=ue(t,S.open);return n&&n.dom.querySelector(".cm-panel-lint ul").focus(),!0},Y=t=>{let e=t.state.field(p,!1);return!e||!e.panel?!1:(t.dispatch({effects:z.of(!1)}),!0)},ye=t=>{let e=t.state.field(p,!1);if(!e)return!1;let n=t.state.selection.main,i=v(e.diagnostics,null,n.to+1);return!i&&(i=v(e.diagnostics,null,0),!i||i.from==n.from&&i.to==n.to)?!1:(t.dispatch({selection:{anchor:i.from,head:i.to},scrollIntoView:!0}),U(t,i.from,1,{tooltip:K,until:o=>o.docChanged||o.newSelection.main.head<i.from||o.newSelection.main.head>i.to}),!0)},Oe=t=>{var e;let{state:n}=t,i=n.field(p,!1);if(!i)return!1;let o=n.selection.main,s,r,l,c;if(i.diagnostics.between(0,n.doc.length,(u,m)=>{m<o.to&&(s==null||s<u)&&(s=u,r=m),(l==null||u>l)&&(l=u,c=m)}),l==null||s==null&&l==o.from)return!1;let a=s??l,f=(e=r??c)!==null&&e!==void 0?e:a;return t.dispatch({selection:{anchor:aLine truncated
|
||||
//# sourceMappingURL=lint.bundle.mjs.map
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/* esm.sh - @codemirror/search@6.7.2 */
|
||||
import{getDialog as ge,showDialog as xe,EditorView as M,Decoration as m,ViewPlugin as ie,showPanel as ye,runScopeHandlers as Me,getPanel as se}from"./cm-view.mjs";import{codePointAt as ve,fromCodePoint as Se,codePointSize as be,EditorSelection as g,Facet as le,combineConfig as oe,CharCategory as p,StateEffect as G,StateField as Ce,RangeSetBuilder as ke,Prec as Le,EditorState as We,findClusterBreak as ce}from"./cm-state.mjs";function u(){var t=arguments[0];typeof t=="string"&&(t=document.createElement(t));var e=1,r=arguments[1];if(r&&typeof r=="object"&&r.nodeType==null&&!Array.isArray(r)){for(var n in r)if(Object.prototype.hasOwnProperty.call(r,n)){var i=r[n];typeof i=="string"?t.setAttribute(n,i):i!=null&&(t[n]=i)}e++}for(;e<arguments.length;e++)ee(t,arguments[e]);return t}function ee(t,e){if(typeof e=="string")t.appendChild(document.createTextNode(e));else if(e!=null)if(e.nodeType!=null)t.appendChild(e);else if(Array.isArray(e))for(var r=0;r<e.length;r++)ee(t,e[r]);else throw new RangeError("Unsupported child node: "+e)}var te=typeof String.prototype.normalize=="function"?t=>t.normalize("NFKD"):t=>t,y=class{constructor(e,r,n=0,i=e.length,s,l){this.test=l,this.value={from:0,to:0,precise:!1},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=e.iterRange(n,i),this.bufferStart=n,this.normalize=s?o=>s(te(o)):te,this.query=this.normalize(r)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return ve(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let e=this.peek();if(e<0)return this.done=!0,this;let r=Se(e),n=this.bufferStart+this.bufferPos;this.bufferPos+=be(e);let i=this.normalize(r);if(i.length)for(let s=0,l=n,o=!0;;s++){let c=i.charCodeAt(s),a=this.match(c,l,o,this.bufferPos+this.bufferStart,s==i.length-1);if(a)return this.value=a,this;if(s==i.length-1)break;o&&s<r.length&&r.charCodeAt(s)==c?l++:o=!1}}}match(e,r,n,i,s){let l=null;for(let o=0;o<this.matches.length;){let c=this.matches[o],a=!1;this.query.charCodeAt(c.index)==e&&(c.index==this.query.length-1?l={from:c.from,to:i,precise:s&&c.precise}:(c.index++,a=!0)),a?o++:this.matches.splice(o,1)}return this.query.charCodeAt(0)==e&&(this.query.length==1?l={from:r,to:i,precise:n&&s}:this.matches.push({from:r,index:1,precise:n})),l&&this.test&&!this.test(l.from,l.to,this.buffer,this.bufferStart)&&(l=null),l}};typeof Symbol<"u"&&(y.prototype[Symbol.iterator]=function(){return this});var ae={from:-1,to:-1,match:/.*/.exec(""),precise:!0},Q="gm"+(/x/.unicode==null?"":"u"),q=class{constructor(e,r,n,i=0,s=e.length){if(this.text=e,this.to=s,this.curLine="",this.done=!1,this.value=ae,/\\[sWDnr]|\n|\r|\[\^/.test(r))return new E(e,r,n,i,s);this.re=new RegExp(r,Q+(n?.ignoreCase?"i":"")),this.test=n?.test,this.iter=e.iter();let l=e.lineAt(i);this.curLineStart=l.from,this.matchPos=R(e,i),this.getLine(this.curLineStart)}getLine(e){this.iter.next(e),this.iter.lineBreak?this.curLine="":(this.curLine=this.iter.value,this.curLineStart+this.curLine.length>this.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let e=this.matchPos-this.curLineStart;;){this.re.lastIndex=e;let r=this.matchPos<=this.to&&this.re.exec(this.curLine);if(r){let n=this.curLineStart+r.index,i=n+r[0].length;if(this.matchPos=R(this.text,i+(n==i?1:0)),n==this.curLineStart+this.curLine.length&&this.nextLine(),(n<i||n>this.value.to)&&(!this.test||this.test(n,i,r)))return this.value={from:n,to:i,precise:!0,match:r},this;e=this.matchPos-this.curLineStart}else if(this.curLineStart+this.curLine.length<this.to)this.nextLine(),e=0;else return this.done=!0,this}}},B=new WeakMap,w=class t{constructor(e,r){this.from=e,this.text=r}get to(){return this.from+this.text.length}static get(e,r,n){let i=B.get(e);if(!i||i.from>=n||i.to<=r){let o=new t(r,e.sliceString(r,n));return B.set(e,o),o}if(i.from==r&&i.to==n)return i;let{text:s,from:l}=i;return l>r&&(s=e.sliceString(r,l)+s,l=r),i.to<n&&(s+=e.sliceString(i.to,n)),B.set(e,new t(l,s)),new t(r,s.slice(r-l,n-l))}},E=class{constructor(e,r,n,i,s){this.text=e,this.to=s,this.done=!1,this.value=ae,this.matchPos=R(e,i),this.re=new RegExp(r,Q+(n?.ignoreCase?"i":"")),this.test=n?.test,this.flat=w.get(e,i,this.chunkEnd(i+5e3))}chunkEnd(e){return e>=this.to?this.to:this.text.lineAt(e).to}next(){for(;;){let e=this.re.lastIndex=this.matchPos-this.flat.from,r=this.re.exec(this.flat.text);if(r&&!r[0]&&r.index==e&&(this.re.lastIndex=e+1,r=this.re.exec(this.flat.text)),r){let n=this.flat.from+r.index,i=n+r[0].length;if((this.flat.to>=this.to||r.index+r[0].length<=this.flat.text.length-10)&&(!this.test||this.test(n,i,r)))return this.value={from:n,to:i,precise:!0,match:r},this.matchPos=R(this.text,i+(Line truncated
|
||||
`:n=="r"?"\r":n=="t"?" ":"\\")}eq(e){return this.search==e.search&&this.replace==e.replace&&this.caseSensitive==e.caseSensitive&&this.regexp==e.regexp&&this.wholeWord==e.wholeWord&&this.test==e.test}create(){return this.regexp?new H(this):new V(this)}getCursor(e,r=0,n){let i=e.doc?e:We.create({doc:e});return n==null&&(n=i.doc.length),this.regexp?b(this,i,r,n):S(this,i,r,n)}},I=class{constructor(e){this.spec=e}};function $e(t,e,r){return(n,i,s,l)=>{if(r&&!r(n,i,s,l))return!1;let o=n>=l&&i<=l+s.length?s.slice(n-l,i-l):e.doc.sliceString(n,i);return t(o,e,n,i)}}function S(t,e,r,n){let i;return t.wholeWord&&(i=_e(e.doc,e.charCategorizer(e.selection.main.head))),t.test&&(i=$e(t.test,e,i)),new y(e.doc,t.unquoted,r,n,t.caseSensitive?void 0:s=>s.toLowerCase(),i)}function _e(t,e){return(r,n,i,s)=>((s>r||s+i.length<n)&&(s=Math.max(0,r-2),i=t.sliceString(s,Math.min(t.length,n+2))),(e(T(i,r-s))!=p.Word||e(O(i,r-s))!=p.Word)&&(e(O(i,n-s))!=p.Word||e(T(i,n-s))!=p.Word))}var V=class extends I{constructor(e){super(e)}nextMatch(e,r,n){let i=S(this.spec,e,n,e.doc.length).nextOverlapping();if(i.done){let s=Math.min(e.doc.length,r+this.spec.unquoted.length);i=S(this.spec,e,0,s).nextOverlapping()}return i.done||i.value.from==r&&i.value.to==n?null:i.value}prevMatchInRange(e,r,n){for(let i=n;;){let s=Math.max(r,i-1e4-this.spec.unquoted.length),l=S(this.spec,e,s,i),o=null;for(;!l.nextOverlapping().done;)o=l.value;if(o)return o;if(s==r)return null;i-=1e4}}prevMatch(e,r,n){let i=this.prevMatchInRange(e,0,r);return i||(i=this.prevMatchInRange(e,Math.max(0,n-this.spec.unquoted.length),e.doc.length)),i&&(i.from!=r||i.to!=n)?i:null}getReplacement(e){return this.spec.unquote(this.spec.replace)}matchAll(e,r){let n=S(this.spec,e,0,e.doc.length),i=[];for(;!n.next().done;){if(i.length>=r)return null;i.push(n.value)}return i}highlight(e,r,n,i){let s=S(this.spec,e,Math.max(0,r-this.spec.unquoted.length),Math.min(n+this.spec.unquoted.length,e.doc.length));for(;!s.next().done;)i(s.value.from,s.value.to)}};function Ne(t,e,r){return(n,i,s)=>(!r||r(n,i,s))&&t(s[0],e,n,i)}function b(t,e,r,n){let i;return t.wholeWord&&(i=ze(e.charCategorizer(e.selection.main.head))),t.test&&(i=Ne(t.test,e,i)),new q(e.doc,t.search,{ignoreCase:!t.caseSensitive,test:i},r,n)}function T(t,e){return t.slice(ce(t,e,!1),e)}function O(t,e){return t.slice(e,ce(t,e))}function ze(t){return(e,r,n)=>!n[0].length||(t(T(n.input,n.index))!=p.Word||t(O(n.input,n.index))!=p.Word)&&(t(O(n.input,n.index+n[0].length))!=p.Word||t(T(n.input,n.index+n[0].length))!=p.Word)}var H=class extends I{nextMatch(e,r,n){let i=b(this.spec,e,n,e.doc.length).next();return i.done&&(i=b(this.spec,e,0,r).next()),i.done?null:i.value}prevMatchInRange(e,r,n){for(let i=1;;i++){let s=Math.max(r,n-i*1e4),l=b(this.spec,e,s,n),o=null;for(;!l.next().done;)o=l.value;if(o&&(s==r||o.from>s+10))return o;if(s==r)return null}}prevMatch(e,r,n){return this.prevMatchInRange(e,0,r)||this.prevMatchInRange(e,n,e.doc.length)}getReplacement(e){return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g,(r,n)=>{if(n=="&")return e.match[0];if(n=="$")return"$";for(let i=n.length;i>0;i--){let s=+n.slice(0,i);if(s>0&&s<e.match.length)return e.match[s]+n.slice(i)}return r})}matchAll(e,r){let n=b(this.spec,e,0,e.doc.length),i=[];for(;!n.next().done;){if(i.length>=r)return null;i.push(n.value)}return i}highlight(e,r,n,i){let s=b(this.spec,e,Math.max(0,r-250),Math.min(n+250,e.doc.length));for(;!s.next().done;)i(s.value.from,s.value.to)}},L=G.define(),U=G.define(),x=Ce.define({create(t){return new k(N(t).create(),null)},update(t,e){for(let r of e.effects)r.is(L)?t=new k(r.value.create(),t.panel):r.is(U)&&(t=new k(t.query,r.value?J:null));return t},provide:t=>ye.from(t,e=>e.panel)});function tt(t){let e=t.field(x,!1);return e?e.query.spec:N(t)}function rt(t){var e;return((e=t.field(x,!1))===null||e===void 0?void 0:e.panel)!=null}var k=class{constructor(e,r){this.query=e,this.panel=r}},Be=m.mark({class:"cm-searchMatch"}),Ve=m.mark({class:"cm-searchMatch cm-searchMatch-selected"}),He=ie.fromClass(class{constructor(t){this.view=t,this.decorations=this.highlight(t.state.field(x))}update(t){let e=t.state.field(x);(e!=t.startState.field(x)||t.docChanged||t.selectionSet||t.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:t,panel:e}){if(!e||!t.spec.valid)return m.none;let{view:r}=this,n=new ke;for(let i=0,s=r.visibleRanges,l=s.length;i<l;i++){let{from:o,to:c}=s[i];for(;i<l-1&&c>s[i+1].from-500;)c=s[++i].to;t.highlight(r.state,o,c,(a,h)=>{let f=r.state.selection.ranges.some(A=>A.from==a&&A.to==h);n.add(a,h,f?Ve:Be)})}return n.finish()}},{decorations:t=>t.decorations});function W(t){return e=>{let r=e.state.field(x,!1);return r&&r.query.spec.valid?t(e,r):de(e)}}var $=W((t,{query:e})=>{let{to:r}=t.state.selection.main,n=e.nextMatch(t.state,r,r);if(!n)return!1;let i=g.single(n.from,n.to),s=t.state.facet(v);return t.dispatch({selection:i,effects:[X(t,n),s.scrollToMatch(i.main,t)],userEvent:"select.search"}),fe(t),!0}),_=W(
Line truncated
|
||||
//# sourceMappingURL=search.bundle.mjs.map
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/* esm.sh - @codemirror/state@6.7.6 */
|
||||
var fe=[],Te=[];(()=>{let s="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,1n,9,16,o,,x,1i,3,,i,,7,a,2,t,3,1k,,,7,2,2,2,3,9,,a,2,q,,2,3,1k,,,5,4,2,2,3,3,,u,2,3,,b,3,1k,,,8,,3,,3,k,2,m,6,,3,1k,,,7,2,2,2,3,7,3,a,2,u,,1n,5,3,3,,4,9,,14,5,1j,,,7,,3,,4,7,2,b,2,t,3,1k,,,7,,3,,4,7,2,b,2,f,,c,4,1j,2,,7,,3,,4,9,,a,2,t,3,1y,,4,6,,,,8,i,2,1p,,,8,c,8,2q,,,a,b,7,21,2,r,,,,,,4,2,1d,k,,2,5,b,,10,9,,2u,b,,6,n,4,4,3,g,4,d,,,3,6,,f,,jj,3,qa,4,s,3,t,2,u,2,1s,w,9,,19,3,,,39,2,y,,3a,c,4,c,63,5,1l,a,,,,,2,o,2,,1c,1a,2,c,k,5,1b,h,12,9,c,3,u,d,1k,e,1c,k,48,3,,l,4,,6,,2,3,5i,1s,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,n,5,4,,2b,2,1e,i,q,i,d,,12,8,p,d,18,4,1b,e,10,,1v,e,c,,8,2,1a,,1f,,,3,2,2,5,2,,,15,5,5,2,6k,8,,2,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,1t,5,8t,2,25,6,1y,b,1d,4,3e,3,1h,f,15,,2,2,a,4,19,b,7,,1p,3,10,e,g,2,18,,c,3,1c,e,8,4,,2,2k,c,6,,2,,4d,c,l,4,1j,2,,7,2,2,2,3,9,,a,2,2,7,3,5,1v,9,,,2,,,4,,5,,,e,2,2a,i,n,,29,k,6j,7,2,9,r,2,2a,h,2y,d,2t,3,2,a,74,f,6t,6,,2,2,4,,,,2,3x,7,2,7,3,,s,a,14,7,,4,8,,9,b,1a,g,5i,8,5j,8,,8,2a,m,,e,3e,6,3,,,2,,7,,,1u,5,,2,,5,9n,4,9,2,,,1c,7,3,5,n,,44l,,6,f,8ug,i,1xc,5,1n,7,t4,,,1j,7,4,29,,b,2,f57,2,3mp,1a,2,n,f2,5,3,6,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,2s,,4g,7,af,,1p,4,e4,4,72,2,6r,,2,,7,2,5,,d6,7,31,7,240,5".split(",").map(e=>e?parseInt(e,36):1);for(let e=0,t=0;e<s.length;e++)(e%2?Te:fe).push(t=t+s[e])})();function Xe(s){if(s<768)return!1;for(let e=0,t=fe.length;;){let n=e+t>>1;if(s<fe[n])t=n;else if(s>=Te[n])e=n+1;else return!0;if(e==t)return!1}}function Ce(s){return s>=127462&&s<=127487}var Oe=8205;function Me(s,e,t=!0,n=!0){return(t?Be:Ye)(s,e,n)}function Be(s,e,t){if(e==s.length)return e;e&&Fe(s.charCodeAt(e))&&Le(s.charCodeAt(e-1))&&e--;let n=ae(s,e);for(e+=Re(n);e<s.length;){let i=ae(s,e);if(n==Oe||i==Oe||t&&Xe(i))e+=Re(i),n=i;else if(Ce(i)){let r=0,l=e-2;for(;l>=0&&Ce(ae(s,l));)r++,l-=2;if(r%2==0)break;e+=2}else break}return e}function Ye(s,e,t){for(;e>1;){let n=Be(s,e-2,t);if(n<e)return n;e--}return 0}function ae(s,e){let t=s.charCodeAt(e);if(!Le(t)||e+1==s.length)return t;let n=s.charCodeAt(e+1);return Fe(n)?(t-55296<<10)+(n-56320)+65536:t}function Fe(s){return s>=56320&&s<57344}function Le(s){return s>=55296&&s<56320}function Re(s){return s<65536?1:2}var w=class s{lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,n){[e,t]=q(this,e,t);let i=[];return this.decompose(0,e,i,2),n.length&&n.decompose(0,n.length,i,3),this.decompose(t,this.length,i,1),V.from(i,this.length-(t-e)+n.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){[e,t]=q(this,e,t);let n=[];return this.decompose(e,t,n,0),V.from(n,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),n=this.length-this.scanIdentical(e,-1),i=new F(this),r=new F(e);for(let l=t,h=t;;){if(i.next(l),r.next(l),l=0,i.lineBreak!=r.lineBreak||i.done!=r.done||i.value!=r.value)return!1;if(h+=i.value.length,i.done||h>=n)return!0}}iter(e=1){return new F(this,e)}iterRange(e,t=this.length){return new te(this,e,t)}iterLines(e,t){let n;if(e==null)n=this.iter();else{t==null&&(t=this.lines+1);let i=this.line(e).from;n=this.iterRange(i,Math.max(i,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new ne(n)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}constructor(){}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?s.empty:e.length<=32?new S(e):V.from(S.split(e,[]))}},S=class s extends w{constructor(e,t=_e(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,n,i){for(let r=0;;r++){let l=this.text[r],h=i+l.length;if((t?n:h)>=e)return new ce(i,h,n,l);i=h+1,n++}}decompose(e,t,n,i){let r=e<=0&&t>=this.length?this:new s(De(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(i&1){let l=n.pop(),h=ee(r.text,l.text.slice(),0,r.length);if(h.length<=32)n.push(new s(h,l.length+r.length));else{let o=h.length>>1;n.push(new s(h.slice(0,o)),new s(h.slice(o)))}}else n.push(r)}replace(e,t,n){if(!(n instanceof s))return super.replace(e,t,n);[e,t]=q(this,e,t);let i=ee(this.text,ee(n.text,De(this.text,0,e)),t),r=this.length+n.length-(t-e);return i.length<=32?new s(i,r):V.from(s.split(i,[]),r)}sliceString(e,t=this.length,n=`
|
||||
`){[e,t]=q(this,e,t);let i="";for(let r=0,l=0;r<=t&&l<this.text.length;l++){let h=this.text[l],o=r+h.length;r>e&&l&&(i+=n),e<o&&t>r&&(i+=h.slice(Math.max(0,e-r),t-r)),r=o+1}return i}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(e,t){let n=[],i=-1;for(let r of e)n.push(r),i+=r.length+1,n.length==32&&(t.push(new s(n,i)),n=[],i=-1);return i>-1&&t.push(new s(n,i)),t}},V=class s extends w{constructor(e,t){super(),this.children=e,this.length=t,this.lines=0;for(let n of e)this.lines+=n.lines}lineInner(e,t,n,i){for(let r=0;;r++){let l=this.children[r],h=i+l.length,o=n+l.lines-1;if((t?o:h)>=e)return l.lineInner(e,t,n,i);i=h+1,n=o+1}}decompose(e,t,n,i){for(let r=0,l=0;l<=t&&r<this.children.length;r++){let h=this.children[r],o=l+h.length;if(e<=o&&t>=l){let a=i&((l<=e?1:0)|(o>=t?2:0));l>=e&&o<=t&&!a?n.push(h):h.decompose(e-l,t-l,n,a)}l=o+1}}replace(e,t,n){if([e,t]=q(this,e,t),n.lines<this.lines)for(let i=0,r=0;i<this.children.length;i++){let l=this.children[i],h=r+l.length;if(e>=r&&t<=h){let o=l.replace(e-r,t-r,n),a=this.lines-l.lines+o.lines;if(o.lines<a>>4&&o.lines>a>>6){let f=this.children.slice();return f[i]=o,new s(f,this.length-(t-e)+n.length)}return super.replace(r,h,o)}r=h+1}return super.replace(e,t,n)}sliceString(e,t=this.length,n=`
|
||||
`){[e,t]=q(this,e,t);let i="";for(let r=0,l=0;r<this.children.length&&l<=t;r++){let h=this.children[r],o=l+h.length;l>e&&r&&(i+=n),e<o&&t>l&&(i+=h.sliceString(e-l,t-l,n)),l=o+1}return i}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(e,t){if(!(e instanceof s))return 0;let n=0,[i,r,l,h]=t>0?[0,0,this.children.length,e.children.length]:[this.children.length-1,e.children.length-1,-1,-1];for(;;i+=t,r+=t){if(i==l||r==h)return n;let o=this.children[i],a=e.children[r];if(o!=a)return n+o.scanIdentical(a,t);n+=o.length+1}}static from(e,t=e.reduce((n,i)=>n+i.length+1,-1)){let n=0;for(let d of e)n+=d.lines;if(n<32){let d=[];for(let g of e)g.flatten(d);return new S(d,t)}let i=Math.max(32,n>>5),r=i<<1,l=i>>1,h=[],o=0,a=-1,f=[];function u(d){let g;if(d.lines>r&&d instanceof s)for(let m of d.children)u(m);else d.lines>l&&(o>l||!o)?(c(),h.push(d)):d instanceof S&&o&&(g=f[f.length-1])instanceof S&&d.lines+g.lines<=32?(o+=d.lines,a+=d.length+1,f[f.length-1]=new S(g.text.concat(d.text),g.length+1+d.length)):(o+d.lines>i&&c(),o+=d.lines,a+=d.length+1,f.push(d))}function c(){o!=0&&(h.push(f.length==1?f[0]:s.from(f,a)),a=-1,o=f.length=0)}for(let d of e)u(d);return c(),h.length==1?h[0]:new s(h,t)}};w.empty=new S([""],0);function _e(s){let e=-1;for(let t of s)e+=t.length+1;return e}function ee(s,e,t=0,n=1e9){for(let i=0,r=0,l=!0;r<s.length&&i<=n;r++){let h=s[r],o=i+h.length;o>=t&&(o>n&&(h=h.slice(0,n-i)),i<t&&(h=h.slice(t-i)),l?(e[e.length-1]+=h,l=!1):e.push(h)),i=o+1}return e}function De(s,e,t){return ee(s,[""],e,t)}var F=class{constructor(e,t=1){this.dir=t,this.done=!1,this.lineBreak=!1,this.value="",this.nodes=[e],this.offsets=[t>0?1:(e instanceof S?e.text.length:e.children.length)<<1]}nextInner(e,t){for(this.done=this.lineBreak=!1;;){let n=this.nodes.length-1,i=this.nodes[n],r=this.offsets[n],l=r>>1,h=i instanceof S?i.text.length:i.children.length;if(l==(t>0?h:0)){if(n==0)return this.done=!0,this.value="",this;t>0&&this.offsets[n-1]++,this.nodes.pop(),this.offsets.pop()}else if((r&1)==(t>0?0:1)){if(this.offsets[n]+=t,e==0)return this.lineBreak=!0,this.value=`
|
||||
`,this;e--}else if(i instanceof S){let o=i.text[l+(t<0?-1:0)];if(this.offsets[n]+=t,o.length>Math.max(0,e))return this.value=e==0?o:t>0?o.slice(e):o.slice(0,o.length-e),this;e-=o.length}else{let o=i.children[l+(t<0?-1:0)];e>o.length?(e-=o.length,this.offsets[n]+=t):(t<0&&this.offsets[n]--,this.nodes.push(o),this.offsets.push(t>0?1:(o instanceof S?o.text.length:o.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}},te=class{constructor(e,t,n){this.value="",this.done=!1,this.cursor=new F(e,t>n?-1:1),this.pos=t>n?e.length:0,this.from=Math.min(t,n),this.to=Math.max(t,n)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let n=t<0?this.pos-this.from:this.to-this.pos;e>n&&(e=n),n-=e;let{value:i}=this.cursor.next(e);return this.pos+=(i.length+e)*t,this.value=i.length<=n?i:t<0?i.slice(i.length-n):i.slice(0,n),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}},ne=class{constructor(e){this.inner=e,this.afterBreak=!0,this.value="",this.done=!1}next(e=0){let{done:t,lineBreak:n,value:i}=this.inner.next(e);return t&&this.afterBreak?(this.value="",this.afterBreak=!1):t?(this.done=!0,this.value=""):n?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=i,this.afterBreak=!1),this}get lineBreak(){return!1}};typeof Symbol<"u"&&(w.prototype[Symbol.iterator]=function(){return this.iter()},F.prototype[Symbol.iterator]=te.prototype[Symbol.iterator]=ne.prototype[Symbol.iterator]=function(){return this});var ce=class{constructor(e,t,n,i){this.from=e,this.to=t,this.number=n,this.text=i}get length(){return this.to-this.from}};function q(s,e,t){return e=Math.max(0,Math.min(s.length,e)),[e,Math.max(e,Math.min(s.length,t))]}function ie(s,e,t=!0,n=!0){return Me(s,e,t,n)}function et(s){return s>=56320&&s<57344}function tt(s){return s>=55296&&s<56320}function gt(s,e){let t=s.charCodeAt(e);if(!tt(t)||e+1==s.length)return t;let n=s.charCodeAt(e+1);return et(n)?(t-55296<<10)+(n-56320)+65536:t}function pt(s){return s<=65535?String.fromCharCode(s):(s-=65536,String.fromCharCode((s>>10)+55296,(s&1023)+56320))}function mt(s){return s<65536?1:2}var de=/\r\n?|\n/,b=(function(s){return s[s.Simple=0]="Simple",s[s.TrackDel=1]="TrackDel",s[s.TrackBefore=2]="TrackBefore",s[s.TrackAfter=3]="TrackAfter",s})(b||(b={})),L=class s{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;t<this.sections.length;t+=2)e+=this.sections[t];return e}get newLength(){let e=0;for(let t=0;t<this.sections.length;t+=2){let n=this.sections[t+1];e+=n<0?this.sections[t]:n}return e}get empty(){return this.sections.length==0||this.sections.length==2&&this.sections[1]<0}iterGaps(e){for(let t=0,n=0,i=0;t<this.sections.length;){let r=this.sections[t++],l=this.sections[t++];l<0?(e(n,i,r),i+=r):i+=l,n+=r}}iterChangedRanges(e,t=!1){ge(this,e,t)}get invertedDesc(){let e=[];for(let t=0;t<this.sections.length;){let n=this.sections[t++],i=this.sections[t++];i<0?e.push(n,i):e.push(i,n)}return new s(e)}composeDesc(e){return this.empty?e:e.empty?this:ze(this,e)}mapDesc(e,t=!1){return e.empty?this:pe(this,e,t)}mapPos(e,t=-1,n=b.Simple){let i=0,r=0;for(let l=0;l<this.sections.length;){let h=this.sections[l++],o=this.sections[l++],a=i+h;if(o<0){if(a>e)return r+(e-i);r+=h}else{if(n!=b.Simple&&a>=e&&(n==b.TrackDel&&i<e&&a>e||n==b.TrackBefore&&i<e||n==b.TrackAfter&&a>e))return null;if(a>e||a==e&&t<0&&!h)return e==i||t<0?r:r+o;r+=o}i=a}if(e>i)throw new RangeError(`Position ${e} is out of range for changeset of length ${i}`);return r}touchesRange(e,t=e){for(let n=0,i=0;n<this.sections.length&&i<=t;){let r=this.sections[n++],l=this.sections[n++],h=i+r;if(l>=0&&i<=t&&h>=e)return i<e&&h>t?"cover":!0;i=h}return!1}toString(){let e="";for(let t=0;t<this.sections.length;){let n=this.sections[t++],i=this.sections[t++];e+=(e?" ":"")+n+(i>=0?":"+i:"")}return e}toJSON(){return this.sections}static fromJSON(e){if(!Array.isArray(e)||e.length%2||e.some(t=>typeof t!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new s(e)}static create(e){return new s(e)}},E=class s extends L{constructor(e,t){super(e),this.inserted=t}apply(e){if(this.length!=e.length)throw new RangeError("Applying change set to a document with the wrong length");return ge(this,(t,n,i,r,l)=>e=e.replace(i,i+(n-t),l),!1),e}mapDesc(e,t=!1){return pe(this,e,t,!0)}invert(e){let t=this.sections.slice(),n=[];for(let i=0,r=0;i<t.length;i+=2){let l=t[i],h=t[i+1];if(h>=0){t[i]=h,t[i+1]=l;let o=i>>1;for(;n.length<o;)n.push(w.empty);n.push(l?e.slice(r,r+l):w.empty)}r+=l}return new s(t,n)}compose(e){return this.empty?e:e.empty?this:ze(this,e,!0)}map(e,t=!1){return e.empty?this:pe(this,e,t,!0)}iterChanges(e,t=!1){ge(this,e,t)}get desc(){return L.create(this.sect
Line truncated
|
||||
`}get readOnly(){return this.facet(Ze)}phrase(e,...t){for(let n of this.facet(s.phrases))if(Object.prototype.hasOwnProperty.call(n,e)){e=n[e];break}return t.length&&(e=e.replace(/\$(\$|\d*)/g,(n,i)=>{if(i=="$")return"$";let r=+(i||1);return!r||r>t.length?n:t[r-1]})),e}languageDataAt(e,t,n=-1){let i=[];for(let r of this.facet(We))for(let l of r(this,t,n))Object.prototype.hasOwnProperty.call(l,e)&&i.push(l[e]);return i}charCategorizer(e){let t=this.languageDataAt("wordChars",e);return ft(t.length?t[0]:"")}wordAt(e){let{text:t,from:n,length:i}=this.doc.lineAt(e),r=this.charCategorizer(e),l=e-n,h=e-n;for(;l>0;){let o=ie(t,l,!1);if(r(t.slice(o,l))!=O.Word)break;l=o}for(;h<i;){let o=ie(t,h);if(r(t.slice(h,o))!=O.Word)break;h=o}return l==h?null:y.range(l+n,h+n)}};P.allowMultipleSelections=we;P.tabSize=I.define({combine:s=>s.length?s[0]:4});P.lineSeparator=je;P.readOnly=Ze;P.phrases=I.define({compare(s,e){let t=Object.keys(s),n=Object.keys(e);return t.length==n.length&&t.every(i=>s[i]==e[i])}});P.languageData=We;P.changeFilter=Ue;P.transactionFilter=Ge;P.transactionExtender=He;re.reconfigure=k.define();function vt(s,e,t={}){let n={};for(let i of s)for(let r of Object.keys(i)){let l=i[r],h=n[r];if(h===void 0)n[r]=l;else if(!(h===l||l===void 0))if(Object.hasOwnProperty.call(t,r))n[r]=t[r](h,l);else throw new Error("Config merge conflict for field "+r)}for(let i in e)n[i]===void 0&&(n[i]=e[i]);return n}var j=class{eq(e){return this==e}range(e,t=e){return K.create(e,t,this)}};j.prototype.startSide=j.prototype.endSide=0;j.prototype.point=!1;j.prototype.mapMode=b.TrackDel;function be(s,e){return s==e||s.constructor==e.constructor&&s.eq(e)}var K=class s{constructor(e,t,n){this.from=e,this.to=t,this.value=n}static create(e,t,n){return new s(e,t,n)}};function Se(s,e){return s.from-e.from||s.value.startSide-e.value.startSide}var Ie=class s{constructor(e,t,n,i){this.from=e,this.to=t,this.value=n,this.maxPoint=i}get length(){return N(this.to)}findIndex(e,t,n,i=0){let r=n?this.to:this.from;for(let l=i,h=r.length;;){if(l==h)return l;let o=l+h>>1,a=r[o]-e||(n?this.value[o].endSide:this.value[o].startSide)-t;if(o==l)return a>=0?l:h;a>=0?h=o:l=o+1}}between(e,t,n,i){for(let r=this.findIndex(t,-1e9,!0),l=this.findIndex(n,1e9,!1,r);r<l;r++)if(i(this.from[r]+e,this.to[r]+e,this.value[r])===!1)return!1}map(e,t,n,i,r){let l=[],h=[],o=[],a=-1,f=-1;e:for(let u=0;u<this.value.length;u++){let c=this.value[u],d=this.from[u]+e,g=this.to[u]+e,m,p;if(d==g){let v=t.mapPos(d,c.startSide,c.mapMode);if(v==null||(m=p=v,c.startSide!=c.endSide&&(p=t.mapPos(d,c.endSide),p<m)))continue}else if(m=t.mapPos(d,c.startSide),p=t.mapPos(g,c.endSide),m>p||m==p&&c.startSide>0&&c.endSide<=0)continue;if(!((p-m||c.endSide-c.startSide)<0))if(a<0&&(a=m),c.point&&(f=Math.max(f,p-m)),(m-n||c.startSide-i)>=0)l.push(c),h.push(m-a),o.push(p-a),n=p,i=c.endSide;else{if(m==p)for(let v=l.length;v>0;v--){if((m-(o[v-1]+a)||c.startSide-l[v-1].endSide)>=0){l.splice(v,0,c),h.splice(v,0,m-a),o.splice(v,0,p-a);continue e}if((m-(h[v-1]+a)||c.endSide-l[v-1].startSide)>0)break}r(m,p,c)}}return{mapped:l.length?new s(h,o,l,f):null,pos:a}}},R=class s{constructor(e,t,n,i){this.chunkPos=e,this.chunk=t,this.nextLayer=n,this.maxPoint=i}static create(e,t,n,i){return new s(e,t,n,i)}get length(){let e=this.chunk.length-1;return e<0?0:Math.max(this.chunkEnd(e),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let e=this.nextLayer.size;for(let t of this.chunk)e+=t.value.length;return e}chunkEnd(e){return this.chunkPos[e]+this.chunk[e].length}update(e){let{add:t=[],sort:n=!1,filterFrom:i=0,filterTo:r=this.length}=e,l=e.filter;if(t.length==0&&!l)return this;if(n&&(t=t.slice().sort(Se)),this.isEmpty)return t.length?s.of(t):this;let h=new oe(this,null,-1).goto(0),o=0,a=[],f=new H;for(;h.value||o<t.length;)if(o<t.length&&(h.from-t[o].from||h.startSide-t[o].value.startSide)>=0){let u=t[o++];f.addInner(u.from,u.to,u.value,!1)||a.push(u)}else h.rangeIndex==1&&h.chunkIndex<this.chunk.length&&(o==t.length||this.chunkEnd(h.chunkIndex)<t[o].from)&&(!l||i>this.chunkEnd(h.chunkIndex)||r<this.chunkPos[h.chunkIndex])&&f.addChunk(this.chunkPos[h.chunkIndex],this.chunk[h.chunkIndex])?h.nextChunk():((!l||i>h.to||r<h.from||l(h.from,h.to,h.value))&&(f.addInner(h.from,h.to,h.value,!1)||a.push(K.create(h.from,h.to,h.value))),h.next());return f.finishInner(this.nextLayer.isEmpty&&!a.length?s.empty:this.nextLayer.update({add:a,filter:l,filterFrom:i,filterTo:r}))}map(e){if(e.empty||this.isEmpty)return this;let t=[],n=[],i=-1,r,l=(o,a,f)=>{r||(r=new H),r.addRange(o,a,f,!1)};for(let o=0;o<this.chunk.length;o++){let a=this.chunkPos[o],f=this.chunk[o],u=e.touchesRange(a,a+f.length);if(u===!1)i=Math.max(i,f.maxPoint),t.push(f),n.push(e.mapPos(a));else if(u===!0){let[c,d]=t.length?[N(n)+N(t).length,N(N(t).value).endSide]:[-1,-1],{mapped:g,pos:m}=f.map(a,e,c,d,l);g&&(i=Math.max(i,g.maxPoint),t.push(g),n.push(m))}}let h=this.nextLayer.map(e);return r&&(h=r.finishInner(h)),t.length==0?h:new s(n,t,h||s.empty,i)}between(e,t,
Line truncated
|
||||
//# sourceMappingURL=state.bundle.mjs.map
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/* esm.sh - @codemirror/view@6.43.15 */
|
||||
import{RangeSet as H,MapMode as Et,RangeValue as ls,findClusterBreak as wt,EditorSelection as C,Facet as k,StateEffect as at,ChangeSet as hn,Text as j,findColumn as li,CharCategory as Cr,EditorState as Yt,Annotation as kr,Transaction as Mr,Prec as as,codePointAt as cn,codePointSize as Ar,combineConfig as hs,StateField as cs,RangeSetBuilder as Tr,countColumn as fn}from"./cm-state.mjs";var vs=typeof Symbol>"u"?"__\u037C":Symbol.for("\u037C"),Je=typeof Symbol>"u"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),Ss=typeof globalThis<"u"?globalThis:typeof window<"u"?window:{},it=class{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(o){return/^@/.test(o)?[o]:o.split(/,\s*/)}function r(o,l,a,h){let c=[],f=/^@(\w+)\b/.exec(o[0]),d=f&&f[1]=="keyframes";if(f&&l==null)return a.push(o[0]+";");for(let p in l){let u=l[p];if(/&/.test(p))r(p.split(/,\s*/).map(g=>o.map(m=>g.replace(/&/,m))).reduce((g,m)=>g.concat(m)),u,a);else if(u&&typeof u=="object"){if(!f)throw new RangeError("The value of a property ("+p+") should be a primitive value.");r(n(p),u,c,d)}else u!=null&&c.push(p.replace(/_.*/,"").replace(/[A-Z]/g,g=>"-"+g.toLowerCase())+": "+u+";")}(c.length||d)&&a.push((i&&!f&&!h?o.map(i):o).join(", ")+" {"+c.join(" ")+"}")}for(let o in t)r(n(o),t[o],this.rules)}getRules(){return this.rules.join(`
|
||||
`)}static newName(){let t=Ss[vs]||1;return Ss[vs]=t+1,"\u037C"+t.toString(36)}static mount(t,e,i){let n=t[Je],r=i&&i.nonce;n?r&&n.setNonce(r):n=new Ze(t,r),n.mount(Array.isArray(e)?e:[e],t)}},Cs=new Map,Ze=class{constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t.head&&t.adoptedStyleSheets&&n.CSSStyleSheet){let r=Cs.get(i);if(r)return t[Je]=r;this.sheet=new n.CSSStyleSheet,Cs.set(i,this)}else this.styleTag=i.createElement("style"),e&&this.styleTag.setAttribute("nonce",e);this.modules=[],t[Je]=this}mount(t,e){let i=this.sheet,n=0,r=0,o=!1;for(let l=0;l<t.length;l++){let a=t[l],h=this.modules.indexOf(a);if(h<r&&h>-1&&(this.modules.splice(h,1),o=!0,r--,h=-1),h==-1){if(this.modules.splice(r++,0,a),o=!0,i)for(let c=0;c<a.rules.length;c++)i.insertRule(a.rules[c],n++)}else{for(;r<h;)n+=this.modules[r++].rules.length;n+=a.rules.length,r++}}if(i)e.adoptedStyleSheets.indexOf(this.sheet)<0&&(e.adoptedStyleSheets=[this.sheet,...e.adoptedStyleSheets]);else{if(o){let a="";for(let h=0;h<this.modules.length;h++)a+=this.modules[h].getRules()+`
|
||||
`;this.styleTag.textContent=a}let l=e.head||e;this.styleTag.parentNode!=l&&l.insertBefore(this.styleTag,l.firstChild)}}setNonce(t){this.styleTag&&this.styleTag.getAttribute("nonce")!=t&&this.styleTag.setAttribute("nonce",t)}};var st={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},Tt={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},vr=typeof navigator<"u"&&/Mac/.test(navigator.platform),Sr=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);for(E=0;E<10;E++)st[48+E]=st[96+E]=String(E);var E;for(E=1;E<=24;E++)st[E+111]="F"+E;var E;for(E=65;E<=90;E++)st[E]=String.fromCharCode(E+32),Tt[E]=String.fromCharCode(E);var E;for(pe in st)Tt.hasOwnProperty(pe)||(Tt[pe]=st[pe]);var pe;function ks(s){var t=vr&&s.metaKey&&s.shiftKey&&!s.ctrlKey&&!s.altKey||Sr&&s.shiftKey&&s.key&&s.key.length==1||s.key=="Unidentified",e=!t&&s.key||(s.shiftKey?Tt:st)[s.keyCode]||s.key||"Unidentified";return e=="Esc"&&(e="Escape"),e=="Del"&&(e="Delete"),e=="Left"&&(e="ArrowLeft"),e=="Up"&&(e="ArrowUp"),e=="Right"&&(e="ArrowRight"),e=="Down"&&(e="ArrowDown"),e}function ft(){var s=arguments[0];typeof s=="string"&&(s=document.createElement(s));var t=1,e=arguments[1];if(e&&typeof e=="object"&&e.nodeType==null&&!Array.isArray(e)){for(var i in e)if(Object.prototype.hasOwnProperty.call(e,i)){var n=e[i];typeof n=="string"?s.setAttribute(i,n):n!=null&&(s[i]=n)}t++}for(;t<arguments.length;t++)Ms(s,arguments[t]);return s}function Ms(s,t){if(typeof t=="string")s.appendChild(document.createTextNode(t));else if(t!=null)if(t.nodeType!=null)s.appendChild(t);else if(Array.isArray(t))for(var e=0;e<t.length;e++)Ms(s,t[e]);else throw new RangeError("Unsupported child node: "+t)}var F=typeof navigator<"u"?navigator:{userAgent:"",vendor:"",platform:""},ai=typeof document<"u"?document:{documentElement:{style:{}}},hi=/Edge\/(\d+)/.exec(F.userAgent),dn=/MSIE \d/.test(F.userAgent),ci=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(F.userAgent),Xe=!!(dn||ci||hi),As=!Xe&&/gecko\/(\d+)/i.test(F.userAgent),ti=!Xe&&/Chrome\/(\d+)/.exec(F.userAgent),Ts="webkitFontSmoothing"in ai.documentElement.style,fi=!Xe&&/Apple Computer/.test(F.vendor),Ds=fi&&(/Mobile\/\w+/.test(F.userAgent)||F.maxTouchPoints>2),y={mac:Ds||/Mac/.test(F.platform),windows:/Win/.test(F.platform),linux:/Linux|X11/.test(F.platform),ie:Xe,ie_version:dn?ai.documentMode||6:ci?+ci[1]:hi?+hi[1]:0,gecko:As,gecko_version:As?+(/Firefox\/(\d+)/.exec(F.userAgent)||[0,0])[1]:0,chrome:!!ti,chrome_version:ti?+ti[1]:0,ios:Ds,android:/Android\b/.test(F.userAgent),webkit:Ts,webkit_version:Ts?+(/\bAppleWebKit\/(\d+)/.exec(F.userAgent)||[0,0])[1]:0,safari:fi,safari_version:fi?+(/\bVersion\/(\d+(\.\d+)?)/.exec(F.userAgent)||[0,0])[1]:0,tabSize:ai.documentElement.style.tabSize!=null?"tab-size":"-moz-tab-size"};function fs(s,t){for(let e in s)e=="class"&&t.class?t.class+=" "+s.class:e=="style"&&t.style?t.style+=";"+s.style:t[e]=s[e];return t}var Re=Object.create(null);function ds(s,t,e){if(s==t)return!0;s||(s=Re),t||(t=Re);let i=Object.keys(s),n=Object.keys(t);if(i.length-0!=n.length-0)return!1;for(let r of i)if(r!=e&&(n.indexOf(r)==-1||s[r]!==t[r]))return!1;return!0}function Dr(s,t){for(let e=s.attributes.length-1;e>=0;e--){let i=s.attributes[e].name;t[i]==null&&s.removeAttribute(i)}for(let e in t){let i=t[e];e=="style"?s.style.cssText=i:s.getAttribute(e)!=i&&s.setAttribute(e,i)}}function Os(s,t,e){let i=!1;if(t)for(let n in t)e&&n in e||(i=!0,n=="style"?s.style.cssText="":s.removeAttribute(n));if(e)for(let n in e)t&&t[n]==e[n]||(i=!0,n=="style"?s.style.cssText=e[n]:s.setAttribute(n,e[n]));return i}function Or(s){let t=Object.create(null);for(let e=0;e<s.attributes.length;e++){let i=s.attributes[e];t[i.name]=i.value}return t}var ot=class{eq(t){return!1}updateDOM(t,e,i){return!1}compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}get estimatedHeight(){return-1}get lineBreaks(){return 0}ignoreEvent(t){return!0}coordsAt(t,e,i){return null}get isHidden(){return!1}get editable(){return!1}destroy(t){}},W=(function(s){return s[s.Text=0]="Text",s[s.WidgetBefore=1]="WidgetBefore",s[s.WidgetAfter=2]="WidgetAfter",s[s.WidgetRange=3]="WidgetRange",s})(W||(W={})),T=class extends ls{constructor(t,e,i,n){super(),this.startSide=t,this.endSide=e,this.widget=i,this.spec=n}get heightRelevant(){return!1}static mark(t){return new se(t)}static widget(t){let e=Math.Line truncated
|
||||
`,n=s.state.doc.line(n.number+(e?1:-1)),r=s.bidiSpans(n),h=e?C.cursor(n.from,-1):C.cursor(n.to,1)}if(a){if(!a(c))return l}else{if(!i)return h;a=i(c)}l=h}}function oo(s,t,e){let i=s.state.charCategorizer(t),n=i(e);return r=>{let o=i(r);return n==Cr.Space&&(n=o),n==o}}function lo(s,t,e,i){let n=t.head,r=e?1:-1;if(n==(e?s.state.doc.length:0))return C.cursor(n,t.assoc);let o=t.goalColumn,l,a=s.contentDOM.getBoundingClientRect(),h=s.coordsAtPos(n,t.assoc||((t.empty?e:t.head==t.from)?1:-1)),c=s.documentTop;if(h)o==null&&(o=h.left-a.left),l=r<0?h.top:h.bottom;else{let u=s.viewState.lineBlockAt(n);o==null&&(o=Math.min(a.right-a.left,s.defaultCharacterWidth*(n-u.from))),l=(r<0?u.top:u.bottom)+c}let f=a.left+o,d=s.viewState.heightOracle.textHeight>>1,p=i??d;for(let u=0;;u+=d){let g=l+(p+u)*r,m=Ti(s,{x:f,y:g},!1,r);if(e?g>a.bottom:g<a.top)return C.cursor(m.pos,m.assoc);let b=s.coordsAtPos(m.pos,m.assoc),S=b?(b.top+b.bottom)/2:0;if(!b||(e?S>l:S<l))return C.cursor(m.pos,m.assoc,void 0,o)}}function Zt(s,t,e){for(;;){let i=0;for(let n of s)n.between(t-1,t+1,(r,o,l)=>{if(t>r&&t<o){let a=i||e||(t-r<o-t?-1:1);t=a<0?r:o,i=a}});if(!i)return t}}function Yn(s,t){let e=null;for(let i=0;i<t.ranges.length;i++){let n=t.ranges[i],r=null;if(n.empty){let o=Zt(s,n.from,0);o!=n.from&&(r=C.cursor(o,-1))}else{let o=Zt(s,n.from,-1),l=Zt(s,n.to,1);(o!=n.from||l!=n.to)&&(n.undirectional?r=C.undirectionalRange(n.from,n.to):r=C.range(n.from==n.anchor?o:l,n.from==n.head?o:l))}r&&(e||(e=t.ranges.slice()),e[i]=r)}return e?C.create(e,t.mainIndex):t}function si(s,t,e){let i=Zt(s.state.facet(ce).map(n=>n(s)),e.from,t.head>e.from?-1:1);return i==e.from?e:C.cursor(i,i<e.from?1:-1)}var _=class{constructor(t,e){this.pos=t,this.assoc=e}};function Ti(s,t,e,i){let n=s.contentDOM.getBoundingClientRect(),r=n.top+s.viewState.paddingTop,{x:o,y:l}=t,a=l-r,h;for(;;){if(a<0)return new _(0,1);if(a>s.viewState.docHeight)return new _(s.state.doc.length,-1);if(h=s.elementAtHeight(a),i==null)break;if(h.type==W.Text){if(i<0?h.to<s.viewport.from:h.from>s.viewport.to)break;let d=s.docView.coordsAt(i<0?h.from:h.to,i>0?-1:1);if(d&&(i<0?d.top<=a+r:d.bottom>=a+r))break}let f=s.viewState.heightOracle.textHeight/2;a=i>0?h.bottom+f:h.top-f}if(s.viewport.from>=h.to||s.viewport.to<=h.from){if(e)return null;if(h.type==W.Text){let f=no(s,n,h,o,l);return new _(f,f==h.from?1:-1)}}if(h.type!=W.Text)return a<(h.top+h.bottom)/2?new _(h.from,1):new _(h.to,-1);let c=s.docView.lineAt(h.from,2);return(!c||c.length!=h.length)&&(c=s.docView.lineAt(h.from,-2)),new Di(s,o,l,s.textDirectionAt(h.from)).scanTile(c,h.from)}var Di=class{constructor(t,e,i,n){this.view=t,this.x=e,this.y=i,this.baseDir=n,this.line=null,this.spans=null}bidiSpansAt(t){return(!this.line||this.line.from>t||this.line.to<t)&&(this.line=this.view.state.doc.lineAt(t),this.spans=this.view.bidiSpans(this.line)),this}baseDirAt(t,e){let{line:i,spans:n}=this.bidiSpansAt(t);return n[Y.find(n,t-i.from,-1,e)].level==this.baseDir}dirAt(t,e){let{line:i,spans:n}=this.bidiSpansAt(t);return n[Y.find(n,t-i.from,-1,e)].dir}bidiIn(t,e){let{spans:i,line:n}=this.bidiSpansAt(t);return i.length>1||i.length&&(i[0].level!=this.baseDir||i[0].to+n.from<e)}scan(t,e,i=!1){let n=0,r=t.length-1,o=new Set,l=this.bidiIn(t[0],t[r]),a,h,c=-1,f=1e9,d;t:for(;n<r;){let u=r-n,g=n+r>>1;e:if(o.has(g)){for(let S=1;S<u;S++){let w=g+S;if(w>=r&&(w-=u),!o.has(w)){g=w;break e}}break t}o.add(g);let m=e(g),b=0;if(m)for(let S=0;S<m.length;S++){let w=m[S];if(!(w.width==0&&m.length>1))if(w.bottom<this.y)(!a||a.bottom<w.bottom)&&(a=w),b=1;else if(w.top>this.y)(!h||h.top>w.top)&&(h=w),b=-1;else{let M=w.left>this.x?this.x-w.left:w.right<this.x?this.x-w.right:0,x=Math.abs(M);x<f&&(c=g,f=x,d=w),M&&(b=M<0==(this.baseDir==L.LTR)?-1:1)}}b==-1&&(!l||this.baseDirAt(t[g],1))?r=g:b==1&&(!l||this.baseDirAt(t[g+1],-1))&&(n=g+1)}if(!d){if(!h&&!a)return{i:0,after:!1};let u=a&&(!h||this.y-a.bottom<h.top-this.y)?a:h;return this.y=(u.top+u.bottom)/2,this.scan(t,e,!0)}if(f&&!i){let{top:u,bottom:g}=d;if(a&&a.bottom>(u+u+g)/3)return this.y=a.bottom-1,this.scan(t,e,!0);if(h&&h.top<(u+g+g)/3)return this.y=h.top+1,this.scan(t,e,!0)}let p=(l?this.dirAt(t[c],1):this.baseDir)==L.LTR;return{i:c,after:this.x>(d.left+d.right)/2==p}}scanText(t,e){let i=[];for(let r=0;r<t.length;r=wt(t.text,r))i.push(e+r);i.push(e+t.length);let n=this.scan(i,r=>{let o=i[r]-e,l=i[r+1]-e;return le(t.dom,o,l).getClientRects()});return n.after?new _(i[n.i+1],-1):new _(i[n.i],1)}scanTile(t,e){if(!t.length)return new _(e,1);if(t.children.length==1){let l=t.children[0];if(l.isText())return this.scanText(l,e);if(l.isComposite())return this.scanTile(l,e)}let i=[e];for(let l=0,a=e;l<t.children.length;l++)i.push(a+=t.children[l].length);let n=this.scan(i,l=>{let a=t.children[l];return a.flags&48?null:(N(a.dom)?a.dom:le(a.dom,0,a.length)).getClientRects()}),r=t.children[n.i],o=i[n.i];return r.isText()?this.scanText(r,o):r.isComposite()?this.scanTile(r,o):n.after?new _(i[n.i+1],-1):new _(o,1)}},Dt="\uFFFF",Oi=class{constructor(t,e){this
Line truncated
|
||||
`&&s.lineWrapping&&(i&&(i=C.single(i.main.anchor-1,i.main.head-1)),e={from:r.from,to:r.to,insert:j.of([" "])}),e)return ys(s,e,i,o);if(i&&!Li(i,r)){let l=!1,a="select";return s.inputState.lastSelectionTime>Date.now()-50&&(s.inputState.lastSelectionOrigin=="select"&&(l=!0),a=s.inputState.lastSelectionOrigin,a=="select.pointer"&&(i=Yn(n.facet(ce).map(h=>h(s)),i))),s.dispatch({selection:i,scrollIntoView:l,userEvent:a}),!0}else return!1}function ys(s,t,e,i=-1){if(y.ios&&s.inputState.flushIOSKey(t))return!0;let n=s.state.selection.main;if(y.android&&(t.to==n.to&&(t.from==n.from||t.from==n.from-1&&s.state.sliceDoc(t.from,n.from)==" ")&&t.insert.length==1&&t.insert.lines==2&&Pt(s.contentDOM,"Enter",13)||(t.from==n.from-1&&t.to==n.to&&t.insert.length==0||i==8&&t.insert.length<t.to-t.from&&t.to>n.head)&&Pt(s.contentDOM,"Backspace",8)||t.from==n.from&&t.to==n.to+1&&t.insert.length==0&&Pt(s.contentDOM,"Delete",46)))return!0;let r=t.insert.toString();s.inputState.composing>=0&&s.inputState.composing++;let o,l=()=>o||(o=co(s,t,e));return s.state.facet(En).some(a=>a(s,t.from,t.to,r,l))||s.dispatch(l()),!0}function co(s,t,e){let i,n=s.state,r=n.selection.main,o=-1;if(t.from==t.to&&t.from<r.from||t.from>r.to){let a=t.from<r.from?-1:1,h=a<0?r.from:r.to,c=Zt(n.facet(ce).map(f=>f(s)),h,a);t.from==c&&(o=c)}if(o>-1)i={changes:t,selection:C.cursor(t.from+t.insert.length,-1)};else if(t.from>=r.from&&t.to<=r.to&&t.to-t.from>=(r.to-r.from)/3&&(!e||e.main.empty&&e.main.from==t.from+t.insert.length)&&s.inputState.composing<0){let a=r.from<t.from?n.sliceDoc(r.from,t.from):"",h=r.to>t.to?n.sliceDoc(t.to,r.to):"";i=n.replaceSelection(s.state.toText(a+t.insert.sliceString(0,void 0,s.state.lineBreak)+h))}else{let a=n.changes(t),h=e&&e.main.to<=a.newLength?e.main:void 0;if(n.selection.ranges.length>1&&(s.inputState.composing>=0||s.inputState.compositionPendingChange)&&t.to<=r.to+10&&t.to>=r.to-10){let c=s.state.sliceDoc(t.from,t.to),f,d=e&&jn(s,e.main.head);if(d){let u=t.insert.length-(t.to-t.from);f={from:d.from,to:d.to-u}}else f=s.state.doc.lineAt(r.head);let p=r.to-t.to;i=n.changeByRange(u=>{if(u.from==r.from&&u.to==r.to)return{changes:a,range:h||u.map(a)};let g=u.to-p,m=g-c.length;if(s.state.sliceDoc(m,g)!=c||g>=f.from&&m<=f.to)return{range:u};let b=n.changes({from:m,to:g,insert:t.insert}),S=u.to-r.to;return{changes:b,range:h?C.range(Math.max(0,h.anchor+S),Math.max(0,h.head+S)):u.map(b)}})}else i={changes:a,selection:h&&n.selection.replaceRange(h)}}let l="input.type";return(s.composing||s.inputState.compositionPendingChange&&s.inputState.compositionEndedAt>Date.now()-50)&&(s.inputState.compositionPendingChange=!1,l+=".compose",s.inputState.compositionFirstChange&&(l+=".start",s.inputState.compositionFirstChange=!1)),n.update(i,{userEvent:l,scrollIntoView:!0})}function Gn(s,t,e,i){let n=Math.min(s.length,t.length),r=0;for(;r<n&&s.charCodeAt(r)==t.charCodeAt(r);)r++;if(r==n&&s.length==t.length)return null;let o=s.length,l=t.length;for(;o>0&&l>0&&s.charCodeAt(o-1)==t.charCodeAt(l-1);)o--,l--;if(i=="end"){let a=Math.max(0,r-Math.min(o,l));e-=o+a-r}if(o<r&&s.length<t.length){let a=e<=r&&e>=o?r-e:0;r-=a,l=r+(l-o),o=r}else if(l<r){let a=e<=r&&e>=l?r-e:0;r-=a,o=r+(o-l),l=r}return{from:r,toA:o,toB:l}}function fo(s){let t=[];if(s.root.activeElement!=s.contentDOM)return t;let{anchorNode:e,anchorOffset:i,focusNode:n,focusOffset:r}=s.observer.selectionRange;return e&&(t.push(new Ne(e,i)),(n!=e||r!=i)&&t.push(new Ne(n,r))),t}function uo(s,t){if(s.length==0)return null;let e=s[0].pos,i=s.length==2?s[1].pos:e;return e<0||i<0?null:e==i?C.create([C.cursor(i+t,-1)]):C.single(e+t,i+t)}function Li(s,t){return t.head==s.main.head&&t.anchor==s.main.anchor}var Bi=class{setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTime=Date.now()}constructor(t){this.view=t,this.lastKeyCode=0,this.lastKeyTime=0,this.touchActive=!1,this.lastTouchTime=0,this.lastTouchX=0,this.lastTouchY=0,this.lastFocusTime=0,this.lastScrollTop=0,this.lastScrollLeft=0,this.lastWheelEvent=0,this.pendingIOSKey=void 0,this.lastIOSMomentumScroll=0,this.tabFocusMode=-1,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastContextMenu=0,this.scrollHandlers=[],this.handlers=Object.create(null),this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.compositionPendingKey=!1,this.compositionPendingChange=!1,this.insertingText="",this.insertingTextAt=0,this.mouseSelection=null,this.draggedContent=null,this.handleEvent=this.handleEvent.bind(this),this.notifiedFocused=t.hasFocus,y.safari&&t.contentDOM.addEventListener("input",()=>null),y.gecko&&Do(t.contentDOM.ownerDocument)}handleEvent(t){!vo(this.view,t)||this.ignoreDuringComposition(t)||t.type=="keydown"&&this.keydown(t)||(this.view.updateState!=0?Promise.resolve().then(()=>this.runHandlers(t.type,t)):this.runHandlers(t.type,t))}runHandlers(t,e){let i=this.handlers[t];if(i){for(let n of i.observers)n(this.view,e);for(let n of i.handlers){if(e.defaultPrevented)break;if(n(this.view,e)){e.preventDef
Line truncated
|
||||
`))};if((y.mac||y.android)&&f.from==o-1&&/^\. ?$/.test(i.text)&&t.contentDOM.getAttribute("autocorrect")=="off"&&(f={from:l,to:a,insert:j.of([i.text.replace("."," ")])}),this.pendingContextChange=f,!t.state.readOnly){let d=this.to-this.from+(f.to-f.from+f.insert.length);ys(t,f,C.single(this.toEditorPos(i.selectionStart,d),this.toEditorPos(i.selectionEnd,d)))}this.pendingContextChange&&(this.revertPending(t.state),this.setSelection(t.state)),f.from<f.to&&!f.insert.length&&t.inputState.composing>=0&&!/[\\p{Alphabetic}\\p{Number}_]/.test(e.text.slice(Math.max(0,i.updateRangeStart-1),Math.min(e.text.length,i.updateRangeStart+1)))&&this.handlers.compositionend(i)},this.handlers.characterboundsupdate=i=>{let n=[],r=null;for(let o=this.toEditorPos(i.rangeStart),l=this.toEditorPos(i.rangeEnd);o<l;o++){let a=t.coordsForChar(o);r=a&&new DOMRect(a.left,a.top,a.right-a.left,a.bottom-a.top)||r||new DOMRect,n.push(r)}e.updateCharacterBounds(i.rangeStart,n)},this.handlers.textformatupdate=i=>{let n=[];for(let r of i.getTextFormats()){let o=r.underlineStyle,l=r.underlineThickness;if(!/none/i.test(o)&&!/none/i.test(l)){let a=this.toEditorPos(r.rangeStart),h=this.toEditorPos(r.rangeEnd);if(a<h){let c=`text-decoration: underline ${/^[a-z]/.test(o)?o+" ":o=="Dashed"?"dashed ":o=="Squiggle"?"wavy ":""}${/thin/i.test(l)?1:2}px`;n.push(T.mark({attributes:{style:c}}).range(a,h))}}}t.dispatch({effects:Vn.of(T.set(n))})},this.handlers.compositionstart=()=>{t.inputState.composing<0&&(t.inputState.composing=0,t.inputState.compositionFirstChange=!0)},this.handlers.compositionend=()=>{if(t.inputState.composing=-1,t.inputState.compositionFirstChange=null,this.composing){let{drifted:i}=this.composing;this.composing=null,i&&this.reset(t.state)}};for(let i in this.handlers)e.addEventListener(i,this.handlers[i]);this.measureReq={read:i=>{let n=re(i.root);n&&n.rangeCount&&this.editContext.updateSelectionBounds(n.getRangeAt(0).getBoundingClientRect())}}}applyEdits(t){let e=0,i=!1,n=this.pendingContextChange;return t.changes.iterChanges((r,o,l,a,h)=>{if(i)return;let c=h.length-(o-r);if(n&&o>=n.to)if(n.from==r&&n.to==o&&n.insert.eq(h)){n=this.pendingContextChange=null,e+=c,this.to+=c;return}else n=null,this.revertPending(t.state);if(r+=e,o+=e,o<=this.from)this.from+=c,this.to+=c;else if(r<this.to){if(r<this.from||o>this.to||this.to-this.from+h.length>3e4){i=!0;return}this.editContext.updateText(this.toContextPos(r),this.toContextPos(o),h.toString()),this.to+=c}e+=c}),n&&!i&&this.revertPending(t.state),!i}update(t){let e=this.pendingContextChange,i=t.startState.selection.main;this.composing&&(this.composing.drifted||!t.changes.touchesRange(i.from,i.to)&&t.transactions.some(n=>!n.isUserEvent("input.type")&&n.changes.touchesRange(this.from,this.to)))?(this.composing.drifted=!0,this.composing.editorBase=t.changes.mapPos(this.composing.editorBase)):!this.applyEdits(t)||!this.rangeIsValid(t.state)?(this.pendingContextChange=null,this.reset(t.state)):(t.docChanged||t.selectionSet||e)&&this.setSelection(t.state),(t.geometryChanged||t.docChanged||t.selectionSet)&&t.view.requestMeasure(this.measureReq)}resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4),this.to=Math.min(t.doc.length,e+1e4)}reset(t){this.resetRange(t),this.editContext.updateText(0,this.editContext.text.length,t.doc.sliceString(this.from,this.to)),this.setSelection(t)}revertPending(t){let e=this.pendingContextChange;this.pendingContextChange=null,this.editContext.updateText(this.toContextPos(e.from),this.toContextPos(e.from+e.insert.length),t.doc.sliceString(e.from,e.to))}setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(this.from,Math.min(this.to,e.anchor))),n=this.toContextPos(e.head);(this.editContext.selectionStart!=i||this.editContext.selectionEnd!=n)&&this.editContext.updateSelection(i,n)}rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-this.from<500||this.to<t.doc.length&&this.to-e<500||this.to-this.from>1e4*3)}toEditorPos(t,e=this.to-this.from){t=Math.min(t,e);let i=this.composing;return i&&i.drifted?i.editorBase+(t-i.contextBase):t+this.from}toContextPos(t){let e=this.composing;return e&&e.drifted?e.contextBase+(t-e.editorBase):t-this.from}destroy(){for(let t in this.handlers)this.editContext.removeEventListener(t,this.handlers[t])}},A=class s{get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return!!this.inputState&&this.inputState.composing>0}get compositionStarted(){return!!this.inputState&&this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}constructor(t={}){var e;this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.clearAnnouncement=-1,this.contentDOM=document.createElement(
Line truncated
|
||||
-\x7F-\x9F\xAD\u061C\u200B\u200E\u200F\u2028\u2029\u202D\u202E\u2066\u2067\u2069\uFEFF\uFFF9-\uFFFC]`,Ui),il={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"},ri=null;function sl(){var s;if(ri==null&&typeof document<"u"&&document.body){let t=document.body.style;ri=((s=t.tabSize)!==null&&s!==void 0?s:t.MozTabSize)!=null}return ri||!1}var Ae=k.define({combine(s){let t=hs(s,{render:null,specialChars:el,addSpecialChars:null});return(t.replaceTabs=!sl())&&(t.specialChars=new RegExp(" |"+t.specialChars.source,Ui)),t.addSpecialChars&&(t.specialChars=new RegExp(t.specialChars.source+"|"+t.addSpecialChars.source,Ui)),t}});function Xl(s={}){return[Ae.of(s),nl()]}var tn=null;function nl(){return tn||(tn=V.fromClass(class{constructor(s){this.view=s,this.decorations=T.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(s.state.facet(Ae)),this.decorations=this.decorator.createDeco(s)}makeDecorator(s){return new ae({regexp:s.specialChars,decoration:(t,e,i)=>{let{doc:n}=e.state,r=cn(t[0],0);if(r==9){let o=n.lineAt(i),l=e.state.tabSize,a=fn(o.text,l,i-o.from);return T.replace({widget:new Ji((l-a%l)*this.view.defaultCharacterWidth/this.view.scaleX)})}return this.decorationCache[r]||(this.decorationCache[r]=T.replace({widget:new Qi(s,r)}))},boundary:s.replaceTabs?void 0:/[^]/})}update(s){let t=s.state.facet(Ae);s.startState.facet(Ae)!=t?(this.decorator=this.makeDecorator(t),this.decorations=this.decorator.createDeco(s.view)):this.decorations=this.decorator.updateDeco(s,this.decorations)}},{decorations:s=>s.decorations}))}var rl="\u2022";function ol(s){return s>=32?rl:s==10?"\u2424":String.fromCharCode(9216+s)}var Qi=class extends ot{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=ol(this.code),i=t.state.phrase("Control character")+" "+(il[this.code]||"0x"+this.code.toString(16)),n=this.options.render&&this.options.render(this.code,i,e);if(n)return n;let r=document.createElement("span");return r.textContent=e,r.title=i,r.setAttribute("aria-label",i),r.className="cm-specialChar",r}ignoreEvent(){return!1}},Ji=class extends ot{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent=" ",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}},en=V.fromClass(class{constructor(){this.height=1e3,this.attrs={style:"padding-bottom: 1000px"}}update(s){let{view:t}=s,e=t.viewState.editorHeight-t.defaultLineHeight-t.documentPadding.top-.5;e>=0&&e!=this.height&&(this.height=e,this.attrs={style:`padding-bottom: ${e}px`})}});function $l(){return[en,$e.of(s=>{var t;return((t=s.plugin(en))===null||t===void 0?void 0:t.attrs)||null})]}function Gl(){return al}var ll=T.line({class:"cm-activeLine"}),al=V.fromClass(class{constructor(s){this.decorations=this.getDeco(s)}update(s){(s.docChanged||s.selectionSet)&&(this.decorations=this.getDeco(s.view))}getDeco(s){let t=-1,e=[];for(let i of s.state.selection.ranges){let n=s.lineBlockAt(i.head);n.from>t&&(e.push(ll.range(n.from)),t=n.from)}return T.set(e)}},{decorations:s=>s.decorations}),Zi=class extends ot{constructor(t){super(),this.content=t}toDOM(t){let e=document.createElement("span");return e.className="cm-placeholder",e.style.pointerEvents="none",e.appendChild(typeof this.content=="string"?document.createTextNode(this.content):typeof this.content=="function"?this.content(t):this.content.cloneNode(!0)),e.setAttribute("aria-hidden","true"),e}coordsAt(t){let e=t.firstChild?$t(t.firstChild):[];if(!e.length)return null;let i=window.getComputedStyle(t.parentNode),n=oe(e[0],i.direction!="rtl"),r=parseInt(i.lineHeight);return n.bottom-n.top>r*1.5?{left:n.left,right:n.right,top:n.top,bottom:n.top+r}:n}ignoreEvent(){return!1}};function Ul(s){let t=V.fromClass(class{constructor(e){this.view=e,this.placeholder=s?T.set([T.widget({widget:new Zi(s),side:1}).range(0)]):T.none}get decorations(){return this.view.state.doc.length?T.none:this.placeholder}},{decorations:e=>e.decorations});return typeof s=="string"?[t,A.contentAttributes.of({"aria-placeholder":s})]:t}var ts=2e3;function hl(s,t,e){let i=Math.min(t.line,e.line),n=Math.max(t.line,e.line),r=[];if(t.off>ts||e.off>ts||t.col<0||e.col<0){let o=Math.min(t.off,e.off),l=Math.max(t.off,e.off);for(let a=i;a<=n;a++){let h=s.doc.line(a);h.length<=l&&r.push(C.range(h.from+o,h.to+l))}}else{let o=Math.min(t.col,e.col),l=Math.max(t.col,e.col);for(let a=i;a<=n;a++){let h=s.doc.line(a),c=li(h.text,o,s.tabSize,!0);if(c<0)r.push(C.cursor(h.to));else{let f=li(h.text,l,s.t
Line truncated
|
||||
//# sourceMappingURL=view.bundle.mjs.map
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
/* esm.sh - @lezer/common@1.5.4 */
|
||||
var De=1024,Pe=0,I=class{constructor(e,t){this.from=e,this.to=t}},k=class{constructor(e={}){this.id=Pe++,this.perNode=!!e.perNode,this.deserialize=e.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")}),this.combine=e.combine||null}add(e){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return typeof e!="function"&&(e=M.match(e)),t=>{let r=e(t);return r===void 0?null:[this,r]}}};k.closedBy=new k({deserialize:s=>s.split(" ")});k.openedBy=new k({deserialize:s=>s.split(" ")});k.group=new k({deserialize:s=>s.split(" ")});k.isolate=new k({deserialize:s=>{if(s&&s!="rtl"&&s!="ltr"&&s!="auto")throw new RangeError("Invalid value for isolate: "+s);return s||"auto"}});k.contextHash=new k({perNode:!0});k.lookAhead=new k({perNode:!0});k.mounted=new k({perNode:!0});var W=class{constructor(e,t,r,n=!1){this.tree=e,this.overlay=t,this.parser=r,this.bracketed=n}static get(e){return e&&e.props&&e.props[k.mounted.id]}},Ee=Object.create(null),M=class s{constructor(e,t,r,n=0){this.name=e,this.props=t,this.id=r,this.flags=n}static define(e){let t=e.props&&e.props.length?Object.create(null):Ee,r=(e.top?1:0)|(e.skipped?2:0)|(e.error?4:0)|(e.name==null?8:0),n=new s(e.name||"",t,e.id,r);if(e.props){for(let i of e.props)if(Array.isArray(i)||(i=i(n)),i){if(i[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");t[i[0].id]=i[1]}}return n}prop(e){return this.props[e.id]}get isTop(){return(this.flags&1)>0}get isSkipped(){return(this.flags&2)>0}get isError(){return(this.flags&4)>0}get isAnonymous(){return(this.flags&8)>0}is(e){if(typeof e=="string"){if(this.name==e)return!0;let t=this.prop(k.group);return t?t.indexOf(e)>-1:!1}return this.id==e}static match(e){let t=Object.create(null);for(let r in e)for(let n of r.split(" "))t[n]=e[r];return r=>{for(let n=r.prop(k.group),i=-1;i<(n?n.length:0);i++){let l=t[i<0?r.name:n[i]];if(l)return l}}}};M.none=new M("",Object.create(null),0,8);var xe=class s{constructor(e){this.types=e;for(let t=0;t<e.length;t++)if(e[t].id!=t)throw new RangeError("Node type ids should correspond to array positions when creating a node set")}extend(...e){let t=[];for(let r of this.types){let n=null;for(let i of e){let l=i(r);if(l){n||(n=Object.assign({},r.props));let f=l[1],h=l[0];h.combine&&h.id in n&&(f=h.combine(n[h.id],f)),n[h.id]=f}}t.push(n?new M(r.name,n,r.id,r.flags):r)}return new s(t)}},Z=new WeakMap,we=new WeakMap,x;(function(s){s[s.ExcludeBuffers=1]="ExcludeBuffers",s[s.IncludeAnonymous=2]="IncludeAnonymous",s[s.IgnoreMounts=4]="IgnoreMounts",s[s.IgnoreOverlays=8]="IgnoreOverlays",s[s.EnterBracketed=16]="EnterBracketed"})(x||(x={}));var E=class s{constructor(e,t,r,n,i){if(this.type=e,this.children=t,this.positions=r,this.length=n,this.props=null,i&&i.length){this.props=Object.create(null);for(let[l,f]of i)this.props[typeof l=="number"?l:l.id]=f}}toString(){let e=W.get(this);if(e&&!e.overlay)return e.tree.toString();let t="";for(let r of this.children){let n=r.toString();n&&(t&&(t+=","),t+=n)}return this.type.name?(/\W/.test(this.type.name)&&!this.type.isError?JSON.stringify(this.type.name):this.type.name)+(t.length?"("+t+")":""):t}cursor(e=0){return new q(this.topNode,e)}cursorAt(e,t=0,r=0){let n=Z.get(this)||this.topNode,i=new q(n);return i.moveTo(e,t),Z.set(this,i._tree),i}get topNode(){return new P(this,0,0,null)}resolve(e,t=0){let r=Q(Z.get(this)||this.topNode,e,t,!1);return Z.set(this,r),r}resolveInner(e,t=0){let r=Q(we.get(this)||this.topNode,e,t,!0);return we.set(this,r),r}resolveStack(e,t=0){return Oe(this,e,t)}iterate(e){let{enter:t,leave:r,from:n=0,to:i=this.length}=e,l=e.mode||0,f=(l&x.IncludeAnonymous)>0;for(let h=this.cursor(l|x.IncludeAnonymous);;){let u=!1;if(h.from<=i&&h.to>=n&&(!f&&h.type.isAnonymous||t(h)!==!1)){if(h.firstChild())continue;u=!0}for(;u&&r&&(f||!h.type.isAnonymous)&&r(h),!h.nextSibling();){if(!h.parent())return;u=!0}}}prop(e){return e.perNode?this.props?this.props[e.id]:void 0:this.type.prop(e)}get propValues(){let e=[];if(this.props)for(let t in this.props)e.push([+t,this.props[t]]);return e}balance(e={}){return this.children.length<=8?this:me(M.none,this.children,this.positions,0,this.children.length,0,this.length,(t,r,n)=>new s(this.type,t,r,n,this.propValues),e.makeTree||((t,r,n)=>new s(M.none,t,r,n)))}static build(e){return Te(e)}};E.empty=new E(M.none,[],[],0);var le=class s{constructor(e,t){this.buffer=e,this.index=t}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new s(this.buffer,this.index)}},H=class s{constructor(e,t,r){this.buffer=e,this.length=t,this.set=r}get type(){return M.none}toString(){let e=[];for(let t=0;t<this.buffer.length;)e.push(this.childString(t)),t=this.buffer[t+3];return e.join(",")}childString(e){let t=this.buffer[e],r=this.buffer[e+3],n=this.set.types[t],i=n.name;if(/\W/Line truncated
|
||||
//# sourceMappingURL=common.bundle.mjs.map
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/* esm.sh - @lezer/highlight@1.2.5 */
|
||||
import{NodeProp as V}from"./lezer-common.mjs";var G=0,p=class s{constructor(t,a,n,i){this.name=t,this.set=a,this.base=n,this.modified=i,this.id=G++}toString(){let{name:t}=this;for(let a of this.modified)a.name&&(t=`${a.name}(${t})`);return t}static define(t,a){let n=typeof t=="string"?t:"?";if(t instanceof s&&(a=t),a?.base)throw new Error("Can not derive from a modified tag");let i=new s(n,[],null,[]);if(i.set.push(i),a)for(let o of a.set)i.set.push(o);return i}static defineModifier(t){let a=new E(t);return n=>n.modified.indexOf(a)>-1?n:E.get(n.base||n,n.modified.concat(a).sort((i,o)=>i.id-o.id))}},L=0,E=class s{constructor(t){this.name=t,this.instances=[],this.id=L++}static get(t,a){if(!a.length)return t;let n=a[0].instances.find(r=>r.base==t&&P(a,r.modified));if(n)return n;let i=[],o=new p(t.name,i,t,a);for(let r of a)r.instances.push(o);let h=Q(a);for(let r of t.set)if(!r.modified.length)for(let d of h)i.push(s.get(r,d));return o}};function P(s,t){return s.length==t.length&&s.every((a,n)=>a==t[n])}function Q(s){let t=[[]];for(let a=0;a<s.length;a++)for(let n=0,i=t.length;n<i;n++)t.push(t[n].concat(s[a]));return t.sort((a,n)=>n.length-a.length)}function _(s){let t=Object.create(null);for(let a in s){let n=s[a];Array.isArray(n)||(n=[n]);for(let i of a.split(" "))if(i){let o=[],h=2,r=i;for(let g=0;;){if(r=="..."&&g>0&&g+3==i.length){h=1;break}let f=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(r);if(!f)throw new RangeError("Invalid path: "+i);if(o.push(f[0]=="*"?"":f[0][0]=='"'?JSON.parse(f[0]):f[0]),g+=f[0].length,g==i.length)break;let m=i[g++];if(g==i.length&&m=="!"){h=0;break}if(m!="/")throw new RangeError("Invalid path: "+i);r=i.slice(g)}let d=o.length-1,c=o[d];if(!c)throw new RangeError("Invalid path: "+i);let x=new v(n,h,d>0?o.slice(0,d):null);t[c]=x.sort(t[c])}}return z.add(t)}var z=new V({combine(s,t){let a,n,i;for(;s||t;){if(!s||t&&s.depth<=t.depth?(i=t,t=t.next):(i=s,s=s.next),a&&a.mode==i.mode&&!i.context&&!a.context)continue;let o=new v(i.tags,i.mode,i.context);a?a.next=o:n=o,a=o}return n}}),v=class{constructor(t,a,n,i){this.tags=t,this.mode=a,this.context=n,this.next=i}get opaque(){return this.mode==0}get inherit(){return this.mode==1}sort(t){return!t||t.depth<this.depth?(this.next=t,this):(t.next=this.sort(t.next),t)}get depth(){return this.context?this.context.length:0}};v.empty=new v([],2,null);function U(s,t){let a=Object.create(null);for(let o of s)if(!Array.isArray(o.tag))a[o.tag.id]=o.class;else for(let h of o.tag)a[h.id]=o.class;let{scope:n,all:i=null}=t||{};return{style:o=>{let h=i;for(let r of o)for(let d of r.set){let c=a[d.id];if(c){h=h?h+" "+c:c;break}}return h},scope:n}}function W(s,t){let a=null;for(let n of s){let i=n.style(t);i&&(a=a?a+" "+i:i)}return a}function X(s,t,a,n=0,i=s.length){let o=new B(n,Array.isArray(t)?t:[t],a);o.highlightRange(s.cursor(),n,i,"",o.highlighters),o.flush(i)}function tt(s,t,a,n,i,o=0,h=s.length){let r=o;function d(c,x){if(!(c<=r)){for(let g=s.slice(r,c),f=0;;){let m=g.indexOf(`
|
||||
`,f),R=m<0?g.length:m;if(R>f&&n(g.slice(f,R),x),m<0)break;i(),f=m+1}r=c}}X(t,a,(c,x,g)=>{d(c,""),d(x,g)},o,h),d(h,"")}var B=class{constructor(t,a,n){this.at=t,this.highlighters=a,this.span=n,this.class=""}startSpan(t,a){a!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=a)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,a,n,i,o){let{type:h,from:r,to:d}=t;if(r>=n||d<=a)return;h.isTop&&(o=this.highlighters.filter(m=>!m.scope||m.scope(h)));let c=i,x=Y(t)||v.empty,g=W(o,x.tags);if(g&&(c&&(c+=" "),c+=g,x.mode==1&&(i+=(i?" ":"")+g)),this.startSpan(Math.max(a,r),c),x.opaque)return;let f=t.tree&&t.tree.prop(V.mounted);if(f&&f.overlay){let m=t.node.enter(f.overlay[0].from+r,1),R=this.highlighters.filter(M=>!M.scope||M.scope(f.tree.type)),H=t.firstChild();for(let M=0,S=r;;M++){let O=M<f.overlay.length?f.overlay[M]:null,K=O?O.from+r:d,$=Math.max(a,S),T=Math.min(n,K);if($<T&&H)for(;t.from<T&&(this.highlightRange(t,$,T,i,o),this.startSpan(Math.min(T,t.to),c),!(t.to>=K||!t.nextSibling())););if(!O||K>n)break;S=O.to+r,S>a&&(this.highlightRange(m.cursor(),Math.max(a,O.from+r),Math.min(n,S),"",R),this.startSpan(Math.min(n,S),c))}H&&t.parent()}else if(t.firstChild()){f&&(i="");do if(!(t.to<=a)){if(t.from>=n)break;this.highlightRange(t,a,n,i,o),this.startSpan(Math.min(n,t.to),c)}while(t.nextSibling());t.parent()}}};function Y(s){let t=s.type.prop(z);for(;t&&t.context&&!s.matchContext(t.context);)t=t.next;return t||null}var e=p.define,C=e(),b=e(),F=e(b),J=e(b),N=e(),I=e(N),j=e(N),y=e(),w=e(y),u=e(),k=e(),D=e(),A=e(D),q=e(),l={comment:C,lineComment:e(C),blockComment:e(C),docComment:e(C),name:b,variableName:e(b),typeName:F,tagName:e(F),propertyName:J,attributeName:e(J),className:e(b),labelName:e(b),namespace:e(b),macroName:e(b),literal:N,string:I,docString:e(I),character:e(I),attributeValue:e(I),number:j,integer:e(j),float:e(j),bool:e(N),regexp:e(N),escape:e(N),color:e(N),url:e(N),keyword:u,self:e(u),null:e(u),atom:e(u),unit:e(u),modifier:e(u),operatorKeyword:e(u),controlKeyword:e(u),definitionKeyword:e(u),moduleKeyword:e(u),operator:k,derefOperator:e(k),arithmeticOperator:e(k),logicOperator:e(k),bitwiseOperator:e(k),compareOperator:e(k),updateOperator:e(k),definitionOperator:e(k),typeOperator:e(k),controlOperator:e(k),punctuation:D,separator:e(D),bracket:A,angleBracket:e(A),squareBracket:e(A),paren:e(A),brace:e(A),content:y,heading:w,heading1:e(w),heading2:e(w),heading3:e(w),heading4:e(w),heading5:e(w),heading6:e(w),contentSeparator:e(y),list:e(y),quote:e(y),emphasis:e(y),strong:e(y),link:e(y),monospace:e(y),strikethrough:e(y),inserted:e(),deleted:e(),changed:e(),invalid:e(),meta:q,documentMeta:e(q),annotation:e(q),processingInstruction:e(q),definition:p.defineModifier("definition"),constant:p.defineModifier("constant"),function:p.defineModifier("function"),standard:p.defineModifier("standard"),local:p.defineModifier("local"),special:p.defineModifier("special")};for(let s in l){let t=l[s];t instanceof p&&(t.name=s)}var et=U([{tag:l.link,class:"tok-link"},{tag:l.heading,class:"tok-heading"},{tag:l.emphasis,class:"tok-emphasis"},{tag:l.strong,class:"tok-strong"},{tag:l.keyword,class:"tok-keyword"},{tag:l.atom,class:"tok-atom"},{tag:l.bool,class:"tok-bool"},{tag:l.url,class:"tok-url"},{tag:l.labelName,class:"tok-labelName"},{tag:l.inserted,class:"tok-inserted"},{tag:l.deleted,class:"tok-deleted"},{tag:l.literal,class:"tok-literal"},{tag:l.string,class:"tok-string"},{tag:l.number,class:"tok-number"},{tag:[l.regexp,l.escape,l.special(l.string)],class:"tok-string2"},{tag:l.variableName,class:"tok-variableName"},{tag:l.local(l.variableName),class:"tok-variableName tok-local"},{tag:l.definition(l.variableName),class:"tok-variableName tok-definition"},{tag:l.special(l.variableName),class:"tok-variableName2"},{tag:l.definition(l.propertyName),class:"tok-propertyName tok-definition"},{tag:l.typeName,class:"tok-typeName"},{tag:l.namespace,class:"tok-namespace"},{tag:l.className,class:"tok-className"},{tag:l.macroName,class:"tok-macroName"},{tag:l.propertyName,class:"tok-propertyName"},{tag:l.operator,class:"tok-operator"},{tag:l.comment,class:"tok-comment"},{tag:l.meta,class:"tok-meta"},{tag:l.invalid,class:"tok-invalid"},{tag:l.punctuation,class:"tok-punctuation"}]);export{p as Tag,et as classHighlighter,Y as getStyleTags,tt as highlightCode,X as highlightTree,_ as styleTags,U as tagHighlighter,l as tags};
|
||||
//# sourceMappingURL=highlight.bundle.mjs.map
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/* esm.sh - @lezer/lr@1.4.11 */
|
||||
import __Process$ from "/node/process.mjs";
|
||||
import{Parser as q,NodeProp as O,NodeSet as J,NodeType as K,DefaultBufferLength as Q,Tree as w,IterMode as V}from"./lezer-common.mjs";var R=class a{constructor(t,e,s,i,h,r,n,o,l,u=0,f){this.p=t,this.stack=e,this.state=s,this.reducePos=i,this.pos=h,this.score=r,this.buffer=n,this.bufferBase=o,this.curContext=l,this.lookAhead=u,this.parent=f}toString(){return`[${this.stack.filter((t,e)=>e%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(t,e,s=0){let i=t.parser.context;return new a(t,[],e,s,s,0,[],0,i?new S(i,i.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(t,e){this.stack.push(this.state,e,this.bufferBase+this.buffer.length),this.state=t}reduce(t){var e;let s=t>>19,i=t&65535,{parser:h}=this.p,r=this.reducePos<this.pos-25&&this.setLookAhead(this.pos),n=h.dynamicPrecedence(i);if(n&&(this.score+=n),s==0){i<h.minRepeatTerm&&this.reducePos<this.pos&&(this.reducePos=this.pos),this.pushState(h.getGoto(this.state,i,!0),this.reducePos),i<h.minRepeatTerm&&this.storeNode(i,this.reducePos,this.reducePos,r?8:4,!0),this.reduceContext(i,this.reducePos);return}let o=this.stack.length-(s-1)*3-(t&262144?6:0),l=o?this.stack[o-2]:this.p.ranges[0].from;i<h.minRepeatTerm&&l==this.reducePos&&this.reducePos<this.pos&&(this.reducePos=this.pos);let u=this.reducePos-l;u>=2e3&&!(!((e=this.p.parser.nodeSet.types[i])===null||e===void 0)&&e.isAnonymous)&&(l==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=u):this.p.lastBigReductionSize<u&&(this.p.bigReductionCount=1,this.p.lastBigReductionStart=l,this.p.lastBigReductionSize=u));let f=o?this.stack[o-1]:0,d=this.bufferBase+this.buffer.length-f;if(i<h.minRepeatTerm||t&131072){let c=h.stateFlag(this.state,1)?this.pos:this.reducePos;this.storeNode(i,l,c,d+4,!0)}if(t&262144)this.state=this.stack[o];else{let c=this.stack[o-3];this.state=h.getGoto(c,i,!0)}for(;this.stack.length>o;)this.stack.pop();this.reduceContext(i,l)}storeNode(t,e,s,i=4,h=!1){if(t==0&&(!this.stack.length||this.stack[this.stack.length-1]<this.buffer.length+this.bufferBase)){let r=this.buffer.length;if(r>0&&this.buffer[r-4]==0&&this.buffer[r-1]>-1){if(e==s)return;if(this.buffer[r-2]>=e){this.buffer[r-2]=s;return}}}if(!h||this.pos==s)this.buffer.push(t,e,s,i);else{let r=this.buffer.length;if(r>0&&(this.buffer[r-4]!=0||this.buffer[r-1]<0)){let n=!1;for(let o=r;o>0&&this.buffer[o-2]>s;o-=4)if(this.buffer[o-1]>=0){n=!0;break}if(n)for(;r>0&&this.buffer[r-2]>s;)this.buffer[r]=this.buffer[r-4],this.buffer[r+1]=this.buffer[r-3],this.buffer[r+2]=this.buffer[r-2],this.buffer[r+3]=this.buffer[r-1],r-=4,i>4&&(i-=4)}this.buffer[r]=t,this.buffer[r+1]=e,this.buffer[r+2]=s,this.buffer[r+3]=i}}shift(t,e,s,i){if(t&131072)this.pushState(t&65535,this.pos);else if((t&262144)==0){let h=t,{parser:r}=this.p;this.pos=i;let n=r.stateFlag(h,1);!n&&(i>s||e<=r.maxNode)&&(this.reducePos=i),this.pushState(h,n?s:Math.min(s,this.reducePos)),this.shiftContext(e,s),e<=r.maxNode&&this.buffer.push(e,s,i,4)}else this.pos=i,this.shiftContext(e,s),e<=this.p.parser.maxNode&&this.buffer.push(e,s,i,4)}apply(t,e,s,i){t&65536?this.reduce(t):this.shift(t,e,s,i)}useNode(t,e,s){let i=this.p.reused.length-1;(i<0||this.p.reused[i]!=t)&&(this.p.reused.push(t),i++);let h=this.reducePos=this.pos=s+t.length;this.pushState(e,s),this.buffer.push(i,s,h,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,t,this,this.p.stream.reset(this.pos-t.length)))}split(){let t=this,e=t.buffer.length;for(e&&t.buffer[e-4]==0&&(e-=4);e>0&&t.buffer[e-2]>t.reducePos;)e-=4;let s=t.buffer.slice(e),i=t.bufferBase+e;for(;t&&i==t.bufferBase;)t=t.parent;return new a(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,s,i,this.curContext,this.lookAhead,t)}recoverByDelete(t,e){let s=t<=this.p.parser.maxNode;s&&this.storeNode(t,this.pos,e,4),this.storeNode(0,this.pos,e,s?8:4),this.pos=this.reducePos=e,this.score-=190}canShift(t){for(let e=new y(this);;){let s=this.p.parser.stateSlot(e.state,4)||this.p.parser.hasAction(e.state,t);if(s==0)return!1;if((s&65536)==0)return!0;e.reduce(s)}}recoverByInsert(t){if(this.stack.length>=300)return[];let e=this.p.parser.nextStates(this.state);if(e.length>8||this.stack.length>=120){let i=[];for(let h=0,r;h<e.length;h+=2)(r=e[h+1])!=this.state&&this.p.parser.hasAction(r,t)&&i.push(e[h],r);if(this.stack.length<120)for(let h=0;i.length<8&&h<e.length;h+=2){let r=e[h+1];i.some((n,o)=>o&1&&n==r)||i.push(e[h],r)}e=i}let s=[];for(let i=0;i<e.length&&s.length<4;i+=2){let h=e[i+1];if(h==this.state)continue;let r=this.split();r.pushState(h,this.pos),r.storeNode(0,r.pos,r.pos,4,!0),r.shiftContext(e[i],this.pos),r.reducePos=this.pos,r.score-=200,s.push(r)}return s}forceReduce(){let{parser:t}=this.p,e=t.stateSlot(this.state,5);if((e&65536)==0)return!1;if(!t.validAction(this.state,e)){let s=e>>19,i=e&65535,h=this.stack.length-s*3;if(h<0||t.getGoto(this.stack[h],i,!1)<0){let r=this.findForcedReduction();if(r==nuLine truncated
|
||||
//# sourceMappingURL=lr.bundle.mjs.map
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"tag": "v2.0.0-beta.1",
|
||||
"commit": "a3b0e660cc4de3d25d75e560afd8441d0fa37a00",
|
||||
"files": {
|
||||
"cm-autocomplete.mjs": {"server": "26fe431a5f54e29952e094c93151f11473a80677bfb339711ecca08195a89df4", "copy": "fcb7fcbe4e2b072d031520ec898c81ac3f2f05fb4126e03003c9b5e60ffaa8de"},
|
||||
"cm-codemirror.mjs": {"server": "dea86a106e13c9316dfedfd29209780b853bdb2b477edf3f7c90f497b592a223", "copy": "0b80ae1289a1dc3e6718727e78bc757caad657e907d0351597867d4f90c36bd2"},
|
||||
"cm-commands.mjs": {"server": "e1868cd8ddb1cbcab5f8acedfaf1ab173d895c0b684b58af573e5991a0ab7dc3", "copy": "20e89b7bc60658df1c5da3b079d4a14f29fc115813062adbac2b16898b283cc8"},
|
||||
"cm-lang-css.mjs": {"server": "2034378ea0d0ce561805bfae0c6b270e945302eedf2717410a750b59d26098fe", "copy": "f969b1b5d2a70deb60cbc1f6634bb9d403c7ab5b92d40713c19688e5be743163"},
|
||||
"cm-lang-html.mjs": {"server": "c431ac565b0b38e77b4900f8e0786a48ad4c74fe3afb1a81253de358ba06ac18", "copy": "3c65a8328577b1604a64b22b161792434a7202b514649c7ddef360f9a1761890"},
|
||||
"cm-lang-javascript.mjs": {"server": "97256d9ab7131a42529bb411fc5271837a215efddd63d4a0a6ddd6bd00d0820e", "copy": "b01ab6b30753bb59ba6ca32308aec293820050be43a9fb4446cfdd572c53ac19"},
|
||||
"cm-lang-json.mjs": {"server": "b58f106e7c0e3d013e0f4a0bde86c4e39b9644576c4244e769271768c6c0af4f", "copy": "74b7041d6d25a3368e666a56312c5444a14180ea1bb10d1e670747b62fe254c5"},
|
||||
"cm-lang-markdown.mjs": {"server": "5b77c954b306115cfda133d7d2535e45e90abcae0e8c4a9dec08fd4f83e16a65", "copy": "0254eafb658a3ed7e8d5eafb17996f5963855007d95c4dedf8a0e3e7c048c82d"},
|
||||
"cm-lang-python.mjs": {"server": "8a9ed958f993dbf2c9cb52200383f7894096ef4b400726fc14ce9737077d1f75", "copy": "d53c16d58fa6c5a47a83ecd012d2cb6c8215996261de5f0cd668ec11273af9a1"},
|
||||
"cm-language.mjs": {"server": "1400a75c21ad190b1fd2050f9d8cfdc948d8d2e4a9467b70ed3953b70498ef5d", "copy": "dfacb8f324365281a1faed43ce8b643f0b663a0e296e9a2e2057c1602b47d3e3"},
|
||||
"cm-lint.mjs": {"server": "88f8d9f4ff305d3a32b6ca44ca78e901c24495b3def2fbb8dc1b351d746f962c", "copy": "5a6b78bd165fab1952d042d786f7fddd3ad229d553f8baedb59cfd119047a65a"},
|
||||
"cm-search.mjs": {"server": "75a5bd0a92b1cda20ca075ae8ce3b42dcaa4847c7f7de3f7ef3e9ec6064157c1", "copy": "ee433bb77e124a717dffce54c9f1203b8838f9740d8ff7023da3ec5ec354fff3"},
|
||||
"cm-state.mjs": {"server": "9db7c46f6be425dbea814af001692939d694b571b3d8df683b87abb635132d4c", "copy": "9db7c46f6be425dbea814af001692939d694b571b3d8df683b87abb635132d4c"},
|
||||
"cm-view.mjs": {"server": "ae7c45442292a6cb1bff0c82b0493924489911ff4f239eb2067293180c0f1d5e", "copy": "7ecdf6913bff51de827d31d56641ccb82f7dfb3801dde0e40c32eadc98815226"},
|
||||
"lezer-common.mjs": {"server": "b63d03ca460cc7a6ce09cbf0de24943fec22bcbe2359dc35978a57b875b2450f", "copy": "b63d03ca460cc7a6ce09cbf0de24943fec22bcbe2359dc35978a57b875b2450f"},
|
||||
"lezer-highlight.mjs": {"server": "b74148d673103ae08330a045a464acddf4ee8b563b213ea26e50f2be01a6d19d", "copy": "df21a1741ae80f2ad2b6d25860cad9ff3cda246a4fba171535d9dc92df150c1a"},
|
||||
"lezer-lr.mjs": {"server": "4c2dcc61b5c3aedb091dfd60ce8abaeb94704b58fa9036547b0b8d2c75f701e4", "copy": "78a97314ddeaa4ae5c251292dcccf0d731ebdca093a59c15793b52131cd50ab4"}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user