15 Commits
Author SHA1 Message Date
HomerandClaude Opus 5.5 2958be0a23 LLeMbas Desktop 1.0.0-beta.2: CLI 2.0.0-beta.2, the look of LLeMbas 2.0.0-beta.4
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
release / linux (push) Failing after 3s
The editor's vendored modules come from server v2.0.0-beta.4 with the
process stub @lezer/lr needs, so languages load and code is highlighted
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:43:28 +00:00
HomerandClaude Opus 5.5 3b0def78ec fetch-vendor: carry the server's process stub for @lezer/lr
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
lezer-lr.mjs imports esm.sh's /node/process.mjs, which the rewrite left
as an absolute path: a 404 in the app, so every parsing language failed.
It becomes ./node-process.mjs, copied from the server at the same ref,
and an absolute import surviving the rewrite now fails the script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:19:13 +00:00
HomerandClaude Opus 5.5 4c2b0d045b LLeMbas Desktop 1.0.0-beta.1: CLI 2.0.0-beta.1, the look of LLeMbas 2.0.0-beta.1
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
release / linux (push) Failing after 3s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:12:36 +00:00
HomerandClaude Opus 5.5 5c41da8b4d CHANGELOG and README: one approval, the files and show routes, the gate
ci / check (push) Failing after 6s
ci / start (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:48:00 +00:00
HomerandClaude Opus 5.5 ab0c1434c4 One approval for app and agent; /api/v2 files and show; app_api 2
- The app's device login asks for `app models` and, where discovery says
  login.device.agent_scope, `agent_scope=models memory link` in the same
  approval. The agent's token goes to the bundled CLI on stdin
  (`lembas login --token-stdin --server <url> --json`) and is never kept
  here; if the CLI does not take it, it is given up on the server. A
  computer linked already keeps its link; a server without agent_scope
  gets the old two steps.
- The server pane reads GET /api/v2/files and /files/content (mounts open
  like folders; `withheld` shown in the server's words). The "not offered
  yet" state is gone.
- A show card's file comes from GET /api/v2/chats/{chat}/show?node=&version=
  or ?shown= as plain text (new `api_text` command, 1 MB cap); args.path is
  read only for agent chats on this computer, locally.
- The gate is link protocol 3 plus app_api 2 (APP_API = 2), not the
  server's version number, which says 1.7.2 on dev until its first beta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:48:00 +00:00
HomerandClaude Opus 5.5 b82d4c688a README and CHANGELOG for Desktop 1.0
ci / check (push) Failing after 4s
ci / start (push) Failing after 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 04:55:03 +00:00
HomerandClaude Opus 5.5 7f54d18375 CI on dev, the app started on X11 and Wayland; no silent re-release
ci.yml runs on dev too, adds the hygiene check, check-ui.sh (the modules parsed
as modules; nothing sets HTML) and the vendor pin, and a start job: a debug
build with the bundled page, started under Xvfb and under a headless Weston with
GTK's Wayland backend, which must still be running after 25 seconds.
release.yml refuses a tag that already has a release unless run by hand with
replace, marks betas as pre-releases, and gives a beta's .deb a ~beta version
so it sorts below its release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 04:55:03 +00:00
HomerandClaude Opus 5.5 4b4e63f622 The app's own interface: agent chats first, two explorers, a viewer
Plain HTML, CSS and ES modules in ui/, no build step. The sidebar opens on agent
chats (this computer first), with chats on the server as the second tab; the
transcript draws the /api/v2 steps, follows a reply as it is written, answers
cards, and starts agent chats in one of the agent's folders. Two explorers kept
apart (this computer's, the server's) and a viewer: Markdown drawn by a safe
renderer that never parses HTML, code in read-only CodeMirror with the server's
token colours, HTML in the sealed frame, show calls opened in it. Settings for
the sign-in, the agent and its folders, notifications and updates; no admin
pages. Layout on fixed and shared tracks; the sidebar is a drawer below 48rem,
the viewer goes over the chat below 64rem. All 124 sentences in the server's
nine languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 04:55:03 +00:00
HomerandClaude Opus 5.5 96a2abe7e0 One window, the app's own page; the server through /api/v2
The two webviews (local title bar, remote web UI) become one WebviewWindow on
ui/index.html. The window refuses every navigation but the app's page and its
sealed frames; the capability still names only main and no remote URL.

- server.rs: discovery refuses a server older than 2.0 (protocol 3 and major
  version 2) and an app API newer than this app knows; the app's own device
  login for the app scope, the token kept in account.json (0600) and never
  given to the page; a proxy limited to /api/v2 paths; the SSE stream.
- files.rs: list and read the agent's working directory, resolved (links and
  .. included) and refused outside the agent's folders.
- sealed.rs: HTML for the viewer on its own scheme under the server's sealed
  policy: no bridge, no network, no navigation.
- watch.rs + tauri-plugin-notification: a notification when a reply or an
  agent chat finishes out of sight.
- A failed start and panics go to desktop.log; a missing tray no longer stops
  the app; the DMA-BUF renderer is off under NVIDIA's driver.
- The unstable multiwebview feature, the gtk dependency and the theme report
  are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 04:55:03 +00:00
HomerandClaude Opus 5.5 af48856fcd The server's 2.0 tokens and its CodeMirror build, pinned
ui/tokens.css from the server's dev (tokens.lock.json; repin to the beta tag
at release). scripts/fetch-vendor.sh copies the server's vendored CodeMirror and
Lezer modules into ui/vendor/, each checked against the server's own pin first,
with the bare module names rewritten to sibling files (the page has no import
map); vendor.lock.json records both hashes, and the script with no argument
verifies the copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 04:55:03 +00:00
HomerandClaude Opus 5.5 b849e1a4b8 CHANGELOG 0.7.2: the reason is in the agent's log, not the status
ci / check (push) Failing after 3s
release / linux (push) Failing after 3s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:52:57 +00:00
HomerandClaude Opus 5.5 f58f28a2a8 LLeMbas Desktop 0.7.2, carrying LLeMbas CLI 1.5.2
ci / check (push) Failing after 4s
Linking this computer again leaves one device, and the agent's status says why
it cannot connect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:52:30 +00:00
HomerandClaude Opus 5.5 8f04494ba8 LLeMbas Desktop 0.7.1, re-released: the fix folded in, no 0.7.2
ci / check (push) Failing after 3s
release / linux (push) Failing after 3s
The first 0.7.1 aborted on Linux before its window appeared (it read
WebKitWebView's write-only 'settings' property). The owner asked for the
patch to be released again rather than followed by a 0.7.2: this commit is
0.7.1 as it should have been, and tag v0.7.1 moves here. The release
workflow now replaces the release of a tag that is pushed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:40:43 +00:00
HomerandClaude Opus 5.5 8062a8ac77 LLeMbas Desktop 0.7.2: starts again on Linux
release / linux (push) Failing after 3s
ci / check (push) Failing after 3s
0.7.1 read WebKitWebView's 'settings' property through GLib; it is write-only,
GLib panicked, and the app aborted before its window appeared. The read is
gone; the microphone permission handler stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:39:32 +00:00
HomerandClaude Opus 5.5 4a982ae089 LLeMbas Desktop 0.7.1, carrying LLeMbas CLI 1.5.1
release / linux (push) Failing after 2s
ci / check (push) Failing after 3s
The microphone: WebKitGTK refuses a permission request its app does not
answer, and has no prompt of its own, so dictation on the server's page was
always blocked. The content view now allows the microphone -- only that, and
only there -- and media capture is switched on explicitly for the older
WebKitGTK the AppImage carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:28:17 +00:00
62 changed files with 7134 additions and 1292 deletions

No files matched your search

+40 -7
View File
@@ -1,9 +1,11 @@
# Checks on every push to main and every pull request: formatting, lints, tests, and the page's # Checks on every push to main and dev and every pull request: formatting, lints, tests, the
# JavaScript. The app is built for real only by release.yml, from a tag. # 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 name: ci
on: on:
push: push:
branches: [main] branches: [main, dev]
pull_request: pull_request:
permissions: permissions:
@@ -23,6 +25,8 @@ jobs:
with: with:
components: rustfmt, clippy components: rustfmt, clippy
cache-workspaces: src-tauri cache-workspaces: src-tauri
- name: Nothing private in a public repository
run: scripts/hygiene.sh
- name: The bundled LLeMbas CLI, verified - name: The bundled LLeMbas CLI, verified
run: scripts/fetch-cli.sh run: scripts/fetch-cli.sh
- name: Formatting - name: Formatting
@@ -34,8 +38,37 @@ jobs:
- name: Tests - name: Tests
working-directory: src-tauri working-directory: src-tauri
run: cargo test --locked run: cargo test --locked
- name: The pages' JavaScript, and the script the content view runs - 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: | run: |
for f in ui/*.js src-tauri/src/*.js; do node --check "$f"; done scripts/fetch-tokens.sh
- name: The design tokens are the server's, as pinned scripts/fetch-vendor.sh
run: scripts/fetch-tokens.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
+41 -4
View File
@@ -1,6 +1,6 @@
# A release, from a signed tag vX.Y.Z on main: the Linux packages (.deb and AppImage) built with # A release, from a signed tag vX.Y.Z (stable, main) or vX.Y.Z-beta.N (a beta, marked as a
# the LLeMbas CLI inside, SHA256SUMS over them, and the GitHub Release with the tag message as its # pre-release): the Linux packages (.deb and AppImage) built with the LLeMbas CLI inside,
# notes. LLeMbas Desktop is developed on Gitea, which mirrors its commits and tags here; the builds # 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. # happen here because they need a full Rust and WebKitGTK toolchain.
name: release name: release
on: on:
@@ -11,6 +11,10 @@ on:
tag: tag:
description: "The tag, e.g. v0.1.0" description: "The tag, e.g. v0.1.0"
required: true 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: permissions:
contents: write contents: write
@@ -48,6 +52,26 @@ jobs:
run: cargo install tauri-cli --version "^2" --locked run: cargo install tauri-cli --version "^2" --locked
- name: Build - name: Build
run: cargo tauri build --bundles deb,appimage 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 - name: Collect the files and SHA256SUMS
run: | run: |
mkdir dist mkdir dist
@@ -62,5 +86,18 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
git tag -l --format='%(contents)' "$TAG" | sed '/-----BEGIN SSH SIGNATURE-----/,$d' > notes.md 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/* --title "LLeMbas Desktop $VERSION" --notes-file notes.md dist/*
+118
View File
@@ -6,6 +6,124 @@ All notable changes to LLeMbas Desktop. The format follows
## [Unreleased] ## [Unreleased]
## [1.0.0-beta.2] — 2026-11-07
Carries LLeMbas CLI 2.0.0-beta.2 and the look of LLeMbas 2.0.0-beta.4.
### Changed
- **The agent keeps what its model has read** (LLeMbas CLI 2.0.0-beta.2, harness spec 3.1.0): the
system prompt is made once per session and sent unchanged, so a local model's server reuses the
whole conversation instead of reading it again; the date and anything that changes during a
session come as a short note on your next message; the tools stay the same for the session;
compaction is the next turn of the same conversation.
### Fixed
- **The editor highlights code again.** One of the editor's modules imported a file the app does
not have, so every language that parses failed to load and files opened as plain text.
## [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 ## [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, Carries LLeMbas CLI 1.5.0: harness spec 2.0.0 — background work as jobs the person can see,
+67 -28
View File
@@ -1,17 +1,24 @@
# LLeMbas Desktop # 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, - **The agent, built in, by default.** LLeMbas Desktop carries the
permissions and credits as in the browser; the app is a front end to the server you already run. [LLeMbas CLI](https://github.com/LLeMbas/LLeMbas-CLI) and runs it in the background as this
- **The agent, built in.** LLeMbas Desktop carries the [LLeMbas CLI](https://github.com/LLeMbas/LLeMbas-CLI) computer's device. Agent chats work in your project folders here, without a terminal -- start
and runs it in the background as this computer's device, so agent chats started on the server them, follow them and answer them in the app.
can work in your projects here, without a terminal. - **Your server's chats** beside them: the same account, models and permissions as in the browser.
- **Its own window**: a LLeMbas title bar with the agent's state and a menu (start and stop the - **Two explorers, kept apart**: the agent's working directory on this computer, and your files
agent, link the computer, reload, change server, quit). Close the window and the agent keeps on the server.
running in the tray, which has the same menu. - **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 ## Install
@@ -20,10 +27,14 @@ Download the `.deb` or the AppImage from the
`SHA256SUMS` in the same release. `SHA256SUMS` in the same release.
```sh ```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 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 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. everything, and starting the app again brings a hidden window back.
@@ -33,20 +44,38 @@ LLeMbas Desktop looks for a new release when it starts and every six hours. When
**update** button appears in the title bar (and *Install LLeMbas x.y.z…* in its menu; *Check for **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 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 itself), then the app starts again. Every package is checked against the release signing key
built into the app before anything is installed. built into the app before anything is installed. Betas are not offered as updates.
## Use ## Use
1. Start LLeMbas and enter your server's address. Sign in there as you would in a browser. 1. Start LLeMbas and enter your server's address. **Sign in**: the approval page opens in your
2. From the tray, **Link this computer**. The approval page opens in the window; approve it. browser, where you are signed in already; approve the code shown in the app. This one approval
3. The agent starts. Choose the folders it may work in (Settings…, *Add folder…*); a new lets the app read and write your chats, as the web interface does, and links this computer as
computer allows none until you do. the device the agent runs on: the app hands the agent's token to its LLeMbas CLI, which keeps it
4. In the server's Agents tab, start an agent chat on **Local** and pick one of those folders. 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 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 (`~/.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 (`/usr/lib/llembas-desktop/`), not on your `PATH`. If you also installed the CLI and run
it and does not start a second one: they are the same device. `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 ## Build
@@ -56,21 +85,31 @@ Needs Rust (stable) and, on Linux, WebKitGTK 4.1 and an AppIndicator library:
sudo apt install libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev 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-cli.sh # the bundled CLI, checked against its release signature
scripts/fetch-tokens.sh # verifies ui/tokens.css against tokens.lock.json 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 install tauri-cli --version "^2" --locked
cargo tauri dev # or: cargo tauri build 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 There is no Node and no bundler: the app's page is plain HTML, CSS and JavaScript modules in
`ui/`. Everything else on screen is the server's own web UI. The version of the CLI it carries is `ui/`. The version of the CLI it carries is in `lembas-cli.version`.
in `lembas-cli.version`.
Two files in `ui/` are generated and committed. `ui/tokens.css` is the server's design tokens, Three things in `ui/` are copied from the LLeMbas server and committed, each pinned:
copied by `scripts/fetch-tokens.sh <server tag>` from a checkout of the server beside this
repository (`../LLeMbas`, or `LEMBAS_SERVER`) and pinned in `tokens.lock.json`; run it with a - `ui/tokens.css`, the server's design tokens: `scripts/fetch-tokens.sh <server ref>` from a
new tag when the server's look changes. `ui/i18n.js` is the pages' words in the server's checkout of the server beside this repository (`../LLeMbas`, or `LEMBAS_SERVER`), pinned in
languages, written by `scripts/gen-i18n.py` from the server's catalogues and the script's own `tokens.lock.json`.
table; run it when a string on the page changes. - `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 ## Licence
MIT; see [LICENSE](LICENSE). The bundled LLeMbas CLI is MIT too, with its own third-party notices. 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
View File
@@ -1 +1 @@
1.5.0 2.0.0-beta.2
+19
View File
@@ -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"
+71
View File
@@ -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"
+113
View File
@@ -0,0 +1,113 @@
#!/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. esm.sh's
# Node polyfill ("/node/process.mjs", which @lezer/lr imports to read process.env.LOG) becomes
# "./node-process.mjs", the server's own one-line stub (static/js/node-process.mjs), copied too:
# an absolute path is a 404 in the app, and a failed @lezer/lr takes every editor language with it.
#
# 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; s#\b(from|import)(\s*)"/node/process\.mjs"#$1$2"./node-process.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 or an absolute path survived in $name; fix the script" >&2
exit 1
fi
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$(sha "$work/out/$name")")")
done
# The polyfill stub, from the server's own scripts (it is not a download, so it has no pin there).
stub="src/lembas/web/static/js/node-process.mjs"
git -C "$SERVER" show "$commit:$stub" > "$work/out/node-process.mjs" \
|| { echo "fetch-vendor: $stub is not in $SERVER at $REF" >&2; exit 1; }
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' node-process.mjs "$(sha "$work/out/node-process.mjs")" "$(sha "$work/out/node-process.mjs")")")
# 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"
+732 -270
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -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"
+100 -3
View File
@@ -469,6 +469,17 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" 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]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.45" version = "0.4.45"
@@ -569,6 +580,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.2" version = "1.5.2"
@@ -1296,6 +1316,7 @@ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi 6.0.0", "r-efi 6.0.0",
"rand_core",
] ]
[[package]] [[package]]
@@ -2064,15 +2085,15 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]] [[package]]
name = "llembas-desktop" name = "llembas-desktop"
version = "0.7.0" version = "1.0.0-beta.2"
dependencies = [ dependencies = [
"gtk",
"libc", "libc",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-updater", "tauri-plugin-updater",
@@ -2094,6 +2115,20 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" 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]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.39.0" version = "0.39.0"
@@ -2220,6 +2255,20 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 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]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.2" version = "0.2.2"
@@ -2842,6 +2891,23 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 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]] [[package]]
name = "raw-window-handle" name = "raw-window-handle"
version = "0.6.2" version = "0.6.2"
@@ -3429,7 +3495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest",
] ]
@@ -3925,6 +3991,26 @@ dependencies = [
"url", "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]] [[package]]
name = "tauri-plugin-opener" name = "tauri-plugin-opener"
version = "2.7.1" version = "2.7.1"
@@ -4097,6 +4183,17 @@ dependencies = [
"toml 1.1.8+spec-1.1.0", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
+4 -7
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "llembas-desktop" name = "llembas-desktop"
version = "0.7.0" version = "1.0.0-beta.2"
description = "LLeMbas Desktop: your LLeMbas server in a window, with the agent on this computer" description = "LLeMbas Desktop: the agent on this computer and your LLeMbas server's chats, in an app of its own"
authors = ["Jaroslav Beneš"] authors = ["Jaroslav Beneš"]
license = "MIT" license = "MIT"
repository = "https://github.com/LLeMbas/LLeMbas-Desktop" repository = "https://github.com/LLeMbas/LLeMbas-Desktop"
@@ -17,8 +17,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = ["tray-icon", "unstable"] } tauri = { version = "2.12", features = ["tray-icon"] }
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-single-instance = "2" tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2" tauri-plugin-updater = "2"
@@ -29,10 +30,6 @@ ureq = { version = "2", default-features = false, features = ["tls", "native-cer
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2" libc = "0.2"
[target.'cfg(target_os = "linux")'.dependencies]
# The window's GTK box: Tauri packs every webview into it with an equal share on Linux.
gtk = "0.18"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
lto = true lto = true
+3 -3
View File
@@ -1,5 +1,5 @@
//! Besides Tauri's own build step: the two themes' colours the window itself paints, read out //! Besides Tauri's own build step: the two themes' page colour, which the window itself paints
//! of `ui/tokens.css` (the server's tokens, fetched by `scripts/fetch-tokens.sh`) into //! 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. //! `$OUT_DIR/tokens.rs`, so no colour is written in Rust. `lib.rs` includes that file.
use std::env; use std::env;
@@ -10,7 +10,7 @@ use std::path::Path;
/// Rust names the constants get. /// Rust names the constants get.
const THEMES: [(&str, &str); 2] = [("moria", "MORIA"), ("shire", "SHIRE")]; const THEMES: [(&str, &str); 2] = [("moria", "MORIA"), ("shire", "SHIRE")];
/// The tokens taken from each theme, by the custom property and the constant's suffix. /// The tokens taken from each theme, by the custom property and the constant's suffix.
const TOKENS: [(&str, &str); 2] = [("--bg", "BG"), ("--border", "BORDER")]; const TOKENS: [(&str, &str); 1] = [("--bg", "BG")];
fn main() { fn main() {
let tokens = Path::new("../ui/tokens.css"); let tokens = Path::new("../ui/tokens.css");
+4 -2
View File
@@ -1,8 +1,10 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "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).", "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"], "windows": [
"main"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
+246
View File
@@ -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();
}
}
+773 -394
View File
File diff suppressed because it is too large. Load diff
+131
View File
@@ -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");
}
}
+772
View File
@@ -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(&not_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();
}
}
+29 -6
View File
@@ -1,22 +1,35 @@
//! What the app remembers: which server it opens, and whether the agent should run. One small //! What the app remembers: which server it talks to, whether the agent should run, the theme and
//! JSON file in the app's config directory; nothing secret is in it (the device's key is the //! 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). //! LLeMbas CLI's, in its own config).
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)] #[serde(default)]
pub struct Settings { pub struct Settings {
/// The server's base address, `https://host[:port]`, with no trailing slash. /// The server's base address, `https://host[:port]`, with no trailing slash.
pub server: Option<String>, pub server: Option<String>,
/// Run the agent while the app runs (once this computer is linked to the server). /// Run the agent while the app runs (once this computer is linked to the server).
pub agent: bool, pub agent: bool,
/// The server page's theme the last time it said (`moria` or `shire`): what the window's /// The account's theme the last time the server said (`moria` or `shire`): what the window
/// frame, the title bar and the first-run page are painted in until the server says again. /// is painted in until it says again. None until it has.
/// None until a server page has been shown.
pub theme: Option<String>, 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 { pub fn file(config_dir: &Path) -> PathBuf {
@@ -85,10 +98,20 @@ mod tests {
let dir = std::env::temp_dir().join(format!("llembas-desktop-test-{}", std::process::id())); let dir = std::env::temp_dir().join(format!("llembas-desktop-test-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&dir);
assert_eq!(load(&dir), Settings::default()); 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 { let s = Settings {
server: Some("https://ai.example".into()), server: Some("https://ai.example".into()),
agent: true, agent: true,
theme: Some("shire".into()), theme: Some("shire".into()),
notify: false,
}; };
save(&dir, &s).unwrap(); save(&dir, &s).unwrap();
assert_eq!(load(&dir), s); assert_eq!(load(&dir), s);
-44
View File
@@ -1,44 +0,0 @@
// Run in the content view on every page it shows (an initialization script, before the page's
// own scripts). It reports the theme the page is in -- the server sets `data-theme` and
// `data-base` on <html> (base.html, then app.js:applyTheme when the reader switches) -- so the
// app can paint the frame and the title bar in the same look.
//
// The report is a navigation to `llembas-theme:<base>`, which the app's navigation handler
// (lib.rs:allowed) reads and refuses, so nothing loads and the page is not left. That is the
// whole channel: the server's pages get no IPC, and this gives them none -- a page can say one
// of two theme names, and nothing else. The base, not the theme's id: a custom theme on the
// server is a handful of colours over `moria` or `shire`, and the base is the look the app has.
(function () {
"use strict";
// Only the server's pages: the app's own first-run page is painted from the app's side.
if (window.top !== window) return;
if (location.protocol !== "http:" && location.protocol !== "https:") return;
if (location.hostname === "tauri.localhost") return;
var last = "";
function report() {
var root = document.documentElement;
if (!root) return;
var base = root.getAttribute("data-base") || root.getAttribute("data-theme") || "";
if (base !== "moria" && base !== "shire") return;
if (base === last) return;
last = base;
try {
location.assign("llembas-theme:" + base);
} catch (e) { /* refused, as intended */ }
}
function watch() {
var root = document.documentElement;
if (!root) return;
new MutationObserver(report).observe(root, {
attributes: true,
attributeFilter: ["data-theme", "data-base"]
});
report();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", watch);
} else {
watch();
}
})();
+139
View File
@@ -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());
}
}
+6 -3
View File
@@ -10,7 +10,10 @@
"withGlobalTauri": true, "withGlobalTauri": true,
"windows": [], "windows": [],
"security": { "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": { "bundle": {
@@ -20,8 +23,8 @@
"appimage" "appimage"
], ],
"category": "Productivity", "category": "Productivity",
"shortDescription": "Your LLeMbas server in a window, with the agent on this computer", "shortDescription": "The LLeMbas agent on this computer, and your server's chats",
"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.", "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", "license": "MIT",
"homepage": "https://llembas.eu", "homepage": "https://llembas.eu",
"publisher": "LLeMbas", "publisher": "LLeMbas",
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"tag": "v1.3.0", "tag": "v2.0.0-beta.4",
"commit": "61cf4c267c983d5895dba3a609f90ae1efd891a0", "commit": "7969067f72605db0df7dd877b58d847156373f18",
"sha256": "aff95291186b57693469cf1f80452fd9a9e460aa360d45ed94c2f4f979e10c6c" "sha256": "9fba389ddb4168034d0ea75e65707f7b98cd53ef2aff9769fb9db7ff01aa9eda"
} }
+614 -99
View File
@@ -1,24 +1,197 @@
/* The first-run page. Every colour, size and space is a token from tokens.css (the server's, /* LLeMbas Desktop's page. Every colour, size, space and font is a token from tokens.css (the
fetched by scripts/fetch-tokens.sh); the one value of this page's own is the card's width. */ 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; } * { box-sizing: border-box; }
:root { :root {
/* The card's widest: a layout fact of this page, not a token the server has. */ /* 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; --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; min-height: 100%; } html, body { margin: 0; height: 100%; overflow: hidden; }
body { body {
display: grid;
place-items: center;
min-height: 100vh;
padding: var(--sp-4);
background: var(--bg); background: var(--bg);
color: var(--ink); color: var(--ink);
font: var(--text-base) / var(--leading-normal) var(--font-body); 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 { .card {
width: min(100%, var(--card-max)); width: min(100%, var(--card-max));
display: grid; display: grid;
@@ -29,77 +202,370 @@ body {
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: var(--shadow-block-lg); box-shadow: var(--shadow-block-lg);
} }
.setup__body { display: grid; gap: var(--sp-6); }
.brand { display: grid; grid-template-columns: var(--sp-10) 1fr; gap: var(--sp-3); align-items: center; } .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 img { width: var(--sp-10); height: var(--sp-10); }
h1 { margin: 0; font: 600 var(--text-xl) / var(--leading-tight) var(--font-ui); } .brand h1 { font: 600 var(--text-xl) / var(--leading-tight) var(--font-ui); }
h1 b { color: var(--leaf); font-weight: 700; } .brand h1 b { color: var(--leaf); font-weight: 700; }
/* A section's heading, in brackets as on the server's screens: `[ SERVER ]`. The brackets are
drawn here so the words alone are what gets translated. */ /* --- The shell ----------------------------------------------------------------------------- */
h2 {
margin: 0 0 var(--sp-3); .shell {
font: 600 var(--text-sm) / var(--leading-tight) var(--font-ui); position: relative;
letter-spacing: 0.08em; min-height: 0;
text-transform: uppercase; display: grid;
color: var(--accent); 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";
} }
h2::before { content: "[ "; }
h2::after { content: " ]"; }
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. */ /* The sidebar: the switch, the new-chat button, the chats, then the two explorers (each a row of
.field { display: grid; grid-template-rows: auto var(--control-h) auto; gap: var(--sp-2); } its own that takes room only while open), then settings. */
label, .label { font: var(--text-sm) / 1.4 var(--font-ui); color: var(--ink-muted); } .side {
.hint { font-size: var(--text-sm); color: var(--ink-muted); min-height: 1.4em; } grid-area: side;
.hint.is-error, .status.is-error { color: var(--warning); } 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);
}
.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); }
.row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); } .switch { display: grid; grid-template-columns: 1fr 1fr; border-bottom: var(--border-w) solid var(--border); }
.row--buttons { grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); margin-top: var(--sp-3); } .switch button {
input {
height: var(--control-h); height: var(--control-h);
min-width: 0; font: 600 var(--text-sm) var(--font-ui);
padding: 0 var(--control-px); color: var(--ink-muted);
font: var(--text-base) var(--font-ui); background: transparent;
border: 0;
border-bottom: var(--border-w-accent) solid transparent;
border-radius: var(--radius-none);
cursor: pointer;
}
.switch button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); background: var(--surface); }
.side__new { margin: var(--sp-3); }
.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); color: var(--ink);
background: var(--bg); background: var(--bg);
border: var(--border-w) solid var(--border-strong); border: var(--border-w) solid var(--border-strong);
border-radius: var(--radius); border-radius: var(--radius);
}
input:focus-visible, .btn:focus-visible {
outline: var(--outline-w) solid var(--accent);
outline-offset: var(--outline-w);
}
.btn {
height: var(--control-h);
padding: 0 var(--sp-4);
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; cursor: pointer;
} }
.btn:hover { background: var(--surface-hover); } .opt[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.btn:active { transform: translate(var(--press), var(--press)); box-shadow: none; } .opt__desc { grid-column: 1; font-size: var(--text-xs); color: var(--ink-muted); }
.btn:disabled { opacity: 0.55; cursor: default; } .opt .chip { grid-column: 2; grid-row: 1; }
.btn--primary { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); } .ask__actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr)); gap: var(--sp-2); }
.btn--primary:hover { background: var(--accent-hover); } .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); }
.status { font: var(--text-sm) / 1.4 var(--font-ui); margin-top: var(--sp-3); } /* The composer: the text and its buttons, on two tracks; the buttons hold their own row. */
.status.is-ok { color: var(--leaf); } .composer {
display: grid;
@media (max-width: 30rem) { grid-template-columns: minmax(0, 1fr) auto;
.card { padding: var(--sp-4); gap: var(--sp-4); } gap: var(--sp-2);
.row { grid-template-columns: 1fr; grid-template-rows: var(--control-h) var(--control-h); } align-items: end;
.field { grid-template-rows: auto auto auto; } 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); }
/* The folder list: every row the same two tracks, the path cut from the left (its end is the /* --- Settings ------------------------------------------------------------------------------ */
part that tells folders apart), the full path in the title. */
.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 { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
.folders li { .folders li {
display: grid; display: grid;
@@ -112,42 +578,91 @@ input:focus-visible, .btn:focus-visible {
border: var(--border-w) solid var(--border); border: var(--border-w) solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
} }
.folders .path { .folders .path { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; direction: rtl; text-align: left; font: var(--text-sm) var(--font-ui); }
overflow: hidden; .folders .btn { height: calc(var(--control-h) - 2 * var(--border-w)); box-shadow: none; border-width: 0 0 0 var(--border-w); }
white-space: nowrap;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
font: var(--text-sm) var(--font-ui);
}
.folders .path bdi { direction: ltr; unicode-bidi: isolate; }
.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); } .folders .empty { padding: var(--control-px); color: var(--warning); font-size: var(--text-sm); }
.settings-grid { /* --- The viewer ---------------------------------------------------------------------------- */
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); .viewer {
grid-auto-rows: auto var(--control-h) auto; grid-area: viewer;
column-gap: var(--sp-4);
row-gap: var(--sp-2);
margin-top: var(--sp-4);
}
/* Each field spans three shared rows (label, control, hint), so the controls of side-by-side
fields sit on one line whatever their labels and hints do. */
.settings-grid .field { grid-row: span 3; grid-template-rows: subgrid; }
select {
height: var(--control-h);
min-width: 0; min-width: 0;
padding: 0 var(--control-px-sm); min-height: 0;
font: var(--text-sm) var(--font-ui); display: grid;
color: var(--ink); grid-template-rows: auto minmax(0, 1fr);
border-left: var(--border-w) solid var(--border);
background: var(--bg); background: var(--bg);
border: var(--border-w) solid var(--border-strong);
border-radius: var(--radius);
} }
.check { display: flex; align-items: center; gap: var(--sp-2); color: var(--ink); } .viewer__head {
.check input { width: var(--check-size); height: var(--check-size); margin: 0; accent-color: var(--accent); } 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) {
#agent-text { display: none; }
.setup .row { grid-template-columns: minmax(0, 1fr); }
.setup .field { grid-template-rows: auto auto auto; }
}
-125
View File
@@ -1,125 +0,0 @@
// The first-run page. It talks to the app over Tauri's IPC — the commands in src-tauri/src/lib.rs.
// The server's own pages never get this: they are remote content with no capability.
// Every word shown goes through `t` (i18n.js, loaded before this), in the system's language.
"use strict"
const { invoke } = window.__TAURI__.core
const { listen } = window.__TAURI__.event
const $ = (id) => document.getElementById(id)
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"],
}
function render(s) {
window.lembasTheme.apply(s.theme)
$("version").textContent = s.version
if (s.server && !$("address").value) $("address").value = s.server
$("agent-section").hidden = !s.server
const noFolders = s.remote && !(s.remote.enabled && s.remote.roots.length)
const [text, cls] =
s.agent.state === "running" && noFolders
? [t("The agent is connected, but may open no folder yet: add one below."), "is-error"]
: (AGENT[s.agent.state] ?? AGENT.off)(s.agent.detail)
const status = $("agent-status")
status.textContent = text
status.className = `status ${cls}`
$("link").textContent = s.linked ? t("Link again") : t("Link this computer")
$("toggle").textContent = s.agent_wanted ? t("Stop the agent") : t("Start the agent")
$("toggle").disabled = !s.linked && !s.agent_wanted
renderFolders(s)
}
function renderFolders(s) {
const r = s.remote
$("folders-section").hidden = !s.server || !s.linked || !r
if (!r) return
const list = $("folders")
list.replaceChildren()
if (!r.enabled || !r.roots.length) {
const li = document.createElement("li")
li.className = "empty"
li.textContent = t("No folder yet: the agent can open nothing. Add the folder of a project.")
list.append(li)
}
for (const dir of r.enabled ? r.roots : []) {
const li = document.createElement("li")
const path = document.createElement("span")
path.className = "path"
path.title = dir
const inner = document.createElement("bdi")
inner.textContent = dir
path.append(inner)
const remove = document.createElement("button")
remove.type = "button"
remove.className = "btn"
remove.textContent = t("Remove")
remove.setAttribute("aria-label", t("Remove %(dir)s", { dir }))
remove.addEventListener("click", () => invoke("remote_remove", { dir }))
li.append(path, remove)
list.append(li)
}
if (r.max_mode) $("max-mode").value = r.max_mode
$("terminal").checked = !!r.terminal
$("folders-status").className = "status"
$("folders-status").textContent = ""
}
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 = t("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 })
})
$("add-folder").addEventListener("click", () => invoke("remote_add"))
$("max-mode").addEventListener("change", (e) => invoke("remote_mode", { mode: e.target.value }))
$("terminal").addEventListener("change", (e) => invoke("remote_terminal", { on: e.target.checked }))
listen("remote-error", (e) => {
$("folders-status").className = "status is-error"
$("folders-status").textContent = String(e.payload)
})
listen("status", (e) => render(e.payload))
refresh()
invoke("remote_refresh")
+608 -24
View File
@@ -11,48 +11,121 @@
const LEMBAS_I18N = { const LEMBAS_I18N = {
"cs": { "cs": {
"%(n)s tokens": "tokeny: %(n)s",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Služba LLeMbas CLI už tu běží (pid %(pid)s); je agentem tohoto počítače.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Služba LLeMbas CLI už tu běží (pid %(pid)s); je agentem tohoto počítače.",
"A chat asking for more is lowered to this.": "Konverzace, která chce víc, je snížena na tohle.", "A chat asking for more is lowered to this.": "Konverzace, která chce víc, je snížena na tohle.",
"A chat asking for more than the folder allows is lowered to it.": "Konverzace, která chce víc, než složka dovoluje, je snížena na to.",
"A shell as you, in one of these folders.": "Shell pod vaším účtem, v jedné z těchto složek.", "A shell as you, in one of these folders.": "Shell pod vaším účtem, v jedné z těchto složek.",
"About": "O aplikaci",
"Add folder…": "Přidat složku…", "Add folder…": "Přidat složku…",
"Address of your LLeMbas server": "Adresa vašeho serveru LLeMbas", "Address of your LLeMbas server": "Adresa vašeho serveru LLeMbas",
"Agent": "Agent", "Agent": "Agent",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Agentní konverzace z vašeho serveru mohou pracovat v těchto složkách a ve všem uvnitř nich — nikde jinde na tomto počítači. Spusťte ji na serveru na kartě Agenti, na zařízení „Místní“.", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Agentní konverzace mohou pracovat v těchto složkách a ve všem uvnitř nich — nikde jinde na tomto počítači.",
"Agent stopped: %(why)s": "Agent se zastavil: %(why)s", "Agent stopped: %(why)s": "Agent se zastavil: %(why)s",
"Allow": "Povolit",
"Always allow": "Vždy povolit",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Agentní konverzace pracuje ve složce na tomto počítači přes agenta vestavěného v této aplikaci.",
"Another device": "Jiné zařízení",
"Answer": "Odpovědět",
"Approve this code in the page that opened in your browser:": "Potvrďte tento kód na stránce, která se otevřela ve vašem prohlížeči:",
"Auto — asks nothing": "Auto — na nic se neptá", "Auto — asks nothing": "Auto — na nic se neptá",
"Cancel": "Zrušit",
"Carry it out": "Provést to",
"Chats": "Konverzace",
"Check for updates…": "Zkontrolovat aktualizace…",
"Close": "Zavřít", "Close": "Zavřít",
"Close (LLeMbas stays in the tray)": "Zavřít (LLeMbas zůstane v oznamovací oblasti)", "Close (LLeMbas stays in the tray)": "Zavřít (LLeMbas zůstane v oznamovací oblasti)",
"Command": "Příkaz",
"Connect": "Připojit", "Connect": "Připojit",
"Copy": "Kopírovat",
"Cut short here; the whole of it is in the web interface.": "Zde zkráceno; celé je to ve webovém rozhraní.",
"Deny": "Zamítnout",
"Earlier messages": "Starší zprávy",
"Edit — asks before commands": "Edit — ptá se před příkazy", "Edit — asks before commands": "Edit — ptá se před příkazy",
"Empty folder": "Prázdná složka",
"Folder": "Složka",
"Install LLeMbas %(version)s…": "Nainstalovat LLeMbas %(version)s…",
"LLeMbas %(version)s is available (this is %(current)s)": "Je k dispozici LLeMbas %(version)s (tohle je %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "Je k dispozici LLeMbas %(version)s (tohle je %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop potřebuje server LLeMbas 2.0 nebo novější.",
"Let the server open a terminal here": "Dovolit serveru otevřít zde terminál", "Let the server open a terminal here": "Dovolit serveru otevřít zde terminál",
"Link again": "Propojit znovu", "Link again": "Propojit znovu",
"Link this computer": "Propojit tento počítač", "Link this computer": "Propojit tento počítač",
"Loading…": "Načítá se…",
"Looking for a LLeMbas server there…": "Hledám tam server LLeMbas…", "Looking for a LLeMbas server there…": "Hledám tam server LLeMbas…",
"Manual — asks first": "Manual — nejdřív se zeptá", "Manual — asks first": "Manual — nejdřív se zeptá",
"Maximise": "Maximalizovat", "Maximise": "Maximalizovat",
"Menu": "Nabídka", "Menu": "Nabídka",
"Minimise": "Minimalizovat", "Minimise": "Minimalizovat",
"Mode": "Režim",
"Model": "Model",
"More": "Více",
"New agent chat": "Nová agentní konverzace",
"New chat": "Nová konverzace",
"No agent chat yet. Start one: it works in a folder on this computer.": "Zatím žádná agentní konverzace. Začněte ji: pracuje ve složce na tomto počítači.",
"No chat yet.": "Zatím žádná konverzace.",
"No folder yet: add one under Settings, Project folders.": "Zatím žádná složka: přidejte ji v Nastavení, Složky projektů.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Zatím žádná složka: agent nemůže nic otevřít. Přidejte složku projektu.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Zatím žádná složka: agent nemůže nic otevřít. Přidejte složku projektu.",
"Open the server": "Otevřít server", "No model is available to this account.": "Tomuto účtu není dostupný žádný model.",
"No project folder yet: the agent can open nothing on this computer.": "Zatím žádná složka projektu: agent na tomto počítači nemůže nic otevřít.",
"Not yet": "Ještě ne",
"Notifications": "Upozornění",
"Only the agent's folders: Settings, Project folders.": "Jen složky agenta: Nastavení, Složky projektů.",
"Open in the viewer": "Otevřít v prohlížeči souborů",
"Open the page again": "Otevřít stránku znovu",
"Or type an answer": "Nebo napište odpověď",
"Other devices": "Jiná zařízení",
"Page": "Stránka",
"Plan — changes nothing": "Plan — nic nemění", "Plan — changes nothing": "Plan — nic nemění",
"Preview": "Náhled",
"Project folders": "Složky projektů", "Project folders": "Složky projektů",
"Read again": "Načíst znovu",
"Recommended": "Doporučeno",
"Remove": "Odebrat", "Remove": "Odebrat",
"Remove %(dir)s": "Odebrat %(dir)s", "Remove %(dir)s": "Odebrat %(dir)s",
"Restore": "Obnovit", "Restore": "Obnovit",
"Send": "Odeslat",
"Server": "Server", "Server": "Server",
"Sign in there as you would in a browser.": "Přihlaste se tam jako v prohlížeči.", "Settings": "Nastavení",
"Shown by the agent": "Ukázal agent",
"Sign in": "Přihlásit se",
"Sign out": "Odhlásit se",
"Signed in as %(who)s.": "Přihlášen jako %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlášením se přihlášení aplikace na serveru zruší; propojení agenta zůstane. Chcete-li použít jiný server, odhlaste se.",
"Source": "Zdroj",
"Start the agent": "Spustit agenta", "Start the agent": "Spustit agenta",
"Stop the agent": "Zastavit agenta", "Stop the agent": "Zastavit agenta",
"Stopped.": "Zastaveno.",
"Taken in while it ran:": "Převzato během běhu:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Upozornit mě, když je odpověď nebo práce agenta hotová, zatímco jsem jinde",
"Terminal": "Terminál", "Terminal": "Terminál",
"The agent asks to run %(tool)s:": "Agent žádá o spuštění %(tool)s:",
"The agent asks:": "Agent se ptá:",
"The agent has finished.": "Agent dokončil práci.",
"The agent is connected, but may open no folder yet: add one below.": "Agent je připojený, ale zatím nesmí otevřít žádnou složku: přidejte ji níže.", "The agent is connected, but may open no folder yet: add one below.": "Agent je připojený, ale zatím nesmí otevřít žádnou složku: přidejte ji níže.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "Agent ještě není připojený k serveru: spusťte ho nebo propojte tento počítač v Nastavení.",
"The agent is off.": "Agent je vypnutý.", "The agent is off.": "Agent je vypnutý.",
"The agent is running: agent chats can use this computer.": "Agent běží: agentní konverzace mohou používat tento počítač.", "The agent is running: agent chats can use this computer.": "Agent běží: agentní konverzace mohou používat tento počítač.",
"The agent is starting…": "Agent se spouští…", "The agent is starting…": "Agent se spouš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.": "Agent umožňuje agentním konverzacím na vašem serveru pracovat v projektech na tomto počítači. Je to vestavěné LLeMbas CLI, které běží na pozadí, dokud běží aplikace.", "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.": "Agent umožňuje agentním konverzacím na vašem serveru pracovat v projektech na tomto počítači. Je to vestavěné LLeMbas CLI, které běží na pozadí, dokud běží aplikace.",
"The agent stopped: %(why)s. Trying again shortly.": "Agent se zastavil: %(why)s. Za chvíli to zkusí znovu.", "The agent stopped: %(why)s. Trying again shortly.": "Agent se zastavil: %(why)s. Za chvíli to zkusí znovu.",
"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.": "Aplikace se k vašemu serveru přihlásí jednou, ve vašem prohlížeči, jako každé zařízení. Pak může číst a psát vaše konverzace, jako webové rozhraní, a stejné schválení propojí tento počítač pro agenta.",
"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.": "Aplikace se k vašemu serveru přihlásí jednou, ve vašem prohlížeči, jako každé zařízení. Pak může číst a psát vaše konverzace, jako webové rozhraní.",
"The connection to the reply was lost: %(why)s": "Spojení s odpovědí se přerušilo: %(why)s",
"The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí", "The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí",
"The reply is ready.": "Odpověď je hotová.",
"Thinking": "Přemýšlí",
"This computer": "Tento počítač",
"This computer is not linked to the server yet.": "Tento počítač ještě není propojený se serverem.", "This computer is not linked to the server yet.": "Tento počítač ještě není propojený se serverem.",
"This file is too big to show here (%(size)s).": "Tento soubor je příliš velký na zobrazení (%(size)s).",
"This is not a text file; it cannot be shown here.": "Toto není textový soubor; nelze ho zde zobrazit.",
"Thought": "Přemýšlel",
"Try again": "Zkusit znovu",
"Waiting for the browser…": "Čeká se na prohlížeč…",
"Waiting for the reply before it": "Čeká na odpověď před ní",
"Waiting for your answer": "Čeká na vaši odpověď",
"Writing…": "Píše se…",
"Your answer": "Vaše odpověď",
"agent off": "agent vypnutý", "agent off": "agent vypnutý",
"agent running": "agent běží", "agent running": "agent běží",
"agent running (service)": "agent běží (služba)", "agent running (service)": "agent běží (služba)",
@@ -64,48 +137,121 @@ const LEMBAS_I18N = {
"update %(version)s": "aktualizace %(version)s" "update %(version)s": "aktualizace %(version)s"
}, },
"de": { "de": {
"%(n)s tokens": "%(n)s Tokens",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Ein LLeMbas-CLI-Dienst läuft hier bereits (PID %(pid)s); er ist der Agent dieses Computers.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Ein LLeMbas-CLI-Dienst läuft hier bereits (PID %(pid)s); er ist der Agent dieses Computers.",
"A chat asking for more is lowered to this.": "Ein Chat, der mehr verlangt, wird hierauf begrenzt.", "A chat asking for more is lowered to this.": "Ein Chat, der mehr verlangt, wird hierauf begrenzt.",
"A chat asking for more than the folder allows is lowered to it.": "Ein Chat, der mehr will, als der Ordner erlaubt, wird darauf herabgesetzt.",
"A shell as you, in one of these folders.": "Eine Shell als Sie, in einem dieser Ordner.", "A shell as you, in one of these folders.": "Eine Shell als Sie, in einem dieser Ordner.",
"About": "Über",
"Add folder…": "Ordner hinzufügen…", "Add folder…": "Ordner hinzufügen…",
"Address of your LLeMbas server": "Adresse Ihres LLeMbas-Servers", "Address of your LLeMbas server": "Adresse Ihres LLeMbas-Servers",
"Agent": "Agent", "Agent": "Agent",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Agent-Chats von Ihrem Server dürfen in diesen Ordnern und allem darin arbeiten — nirgends sonst auf diesem Computer. Starten Sie einen im Reiter Agenten des Servers, auf „Lokal“.", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Agenten-Chats können in diesen Ordnern und allem darin arbeiten — nirgendwo sonst auf diesem Computer.",
"Agent stopped: %(why)s": "Agent angehalten: %(why)s", "Agent stopped: %(why)s": "Agent angehalten: %(why)s",
"Allow": "Erlauben",
"Always allow": "Immer erlauben",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Ein Agenten-Chat arbeitet in einem Ordner auf diesem Computer, über den in diese App eingebauten Agenten.",
"Another device": "Ein anderes Gerät",
"Answer": "Antworten",
"Approve this code in the page that opened in your browser:": "Bestätigen Sie diesen Code auf der Seite, die sich in Ihrem Browser geöffnet hat:",
"Auto — asks nothing": "Auto — fragt nichts", "Auto — asks nothing": "Auto — fragt nichts",
"Cancel": "Abbrechen",
"Carry it out": "Ausführen",
"Chats": "Chats",
"Check for updates…": "Nach Updates suchen…",
"Close": "Schließen", "Close": "Schließen",
"Close (LLeMbas stays in the tray)": "Schließen (LLeMbas bleibt im Benachrichtigungsbereich)", "Close (LLeMbas stays in the tray)": "Schließen (LLeMbas bleibt im Benachrichtigungsbereich)",
"Command": "Befehl",
"Connect": "Verbinden", "Connect": "Verbinden",
"Copy": "Kopieren",
"Cut short here; the whole of it is in the web interface.": "Hier gekürzt; vollständig steht es in der Weboberfläche.",
"Deny": "Ablehnen",
"Earlier messages": "Frühere Nachrichten",
"Edit — asks before commands": "Edit — fragt vor Befehlen", "Edit — asks before commands": "Edit — fragt vor Befehlen",
"Empty folder": "Leerer Ordner",
"Folder": "Ordner",
"Install LLeMbas %(version)s…": "LLeMbas %(version)s installieren…",
"LLeMbas %(version)s is available (this is %(current)s)": "LLeMbas %(version)s ist verfügbar (dies ist %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "LLeMbas %(version)s ist verfügbar (dies ist %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop braucht einen LLeMbas-Server 2.0 oder neuer.",
"Let the server open a terminal here": "Dem Server erlauben, hier ein Terminal zu öffnen", "Let the server open a terminal here": "Dem Server erlauben, hier ein Terminal zu öffnen",
"Link again": "Erneut verbinden", "Link again": "Erneut verbinden",
"Link this computer": "Diesen Computer verbinden", "Link this computer": "Diesen Computer verbinden",
"Loading…": "Wird geladen…",
"Looking for a LLeMbas server there…": "Suche dort nach einem LLeMbas-Server…", "Looking for a LLeMbas server there…": "Suche dort nach einem LLeMbas-Server…",
"Manual — asks first": "Manual — fragt zuerst", "Manual — asks first": "Manual — fragt zuerst",
"Maximise": "Maximieren", "Maximise": "Maximieren",
"Menu": "Menü", "Menu": "Menü",
"Minimise": "Minimieren", "Minimise": "Minimieren",
"Mode": "Modus",
"Model": "Modell",
"More": "Mehr",
"New agent chat": "Neuer Agenten-Chat",
"New chat": "Neuer Chat",
"No agent chat yet. Start one: it works in a folder on this computer.": "Noch kein Agenten-Chat. Starten Sie einen: Er arbeitet in einem Ordner auf diesem Computer.",
"No chat yet.": "Noch kein Chat.",
"No folder yet: add one under Settings, Project folders.": "Noch kein Ordner: Fügen Sie einen unter Einstellungen, Projektordner hinzu.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Noch kein Ordner: Der Agent kann nichts öffnen. Fügen Sie den Ordner eines Projekts hinzu.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Noch kein Ordner: Der Agent kann nichts öffnen. Fügen Sie den Ordner eines Projekts hinzu.",
"Open the server": "Server öffnen", "No model is available to this account.": "Diesem Konto steht kein Modell zur Verfügung.",
"No project folder yet: the agent can open nothing on this computer.": "Noch kein Projektordner: Der Agent kann auf diesem Computer nichts öffnen.",
"Not yet": "Noch nicht",
"Notifications": "Benachrichtigungen",
"Only the agent's folders: Settings, Project folders.": "Nur die Ordner des Agenten: Einstellungen, Projektordner.",
"Open in the viewer": "Im Betrachter öffnen",
"Open the page again": "Seite erneut öffnen",
"Or type an answer": "Oder eine Antwort eingeben",
"Other devices": "Andere Geräte",
"Page": "Seite",
"Plan — changes nothing": "Plan — ändert nichts", "Plan — changes nothing": "Plan — ändert nichts",
"Preview": "Vorschau",
"Project folders": "Projektordner", "Project folders": "Projektordner",
"Read again": "Neu lesen",
"Recommended": "Empfohlen",
"Remove": "Entfernen", "Remove": "Entfernen",
"Remove %(dir)s": "%(dir)s entfernen", "Remove %(dir)s": "%(dir)s entfernen",
"Restore": "Wiederherstellen", "Restore": "Wiederherstellen",
"Send": "Senden",
"Server": "Server", "Server": "Server",
"Sign in there as you would in a browser.": "Melden Sie sich dort wie im Browser an.", "Settings": "Einstellungen",
"Shown by the agent": "Vom Agenten gezeigt",
"Sign in": "Anmelden",
"Sign out": "Abmelden",
"Signed in as %(who)s.": "Angemeldet als %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Beim Abmelden gibt die App ihre Anmeldung am Server auf; die Verbindung des Agenten bleibt. Melden Sie sich ab, um einen anderen Server zu verwenden.",
"Source": "Quelltext",
"Start the agent": "Agent starten", "Start the agent": "Agent starten",
"Stop the agent": "Agent anhalten", "Stop the agent": "Agent anhalten",
"Stopped.": "Angehalten.",
"Taken in while it ran:": "Während des Laufs übernommen:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Benachrichtigen, wenn eine Antwort oder die Arbeit des Agenten fertig ist, während ich woanders bin",
"Terminal": "Terminal", "Terminal": "Terminal",
"The agent asks to run %(tool)s:": "Der Agent möchte %(tool)s ausführen:",
"The agent asks:": "Der Agent fragt:",
"The agent has finished.": "Der Agent ist fertig.",
"The agent is connected, but may open no folder yet: add one below.": "Der Agent ist verbunden, darf aber noch keinen Ordner öffnen: Fügen Sie unten einen hinzu.", "The agent is connected, but may open no folder yet: add one below.": "Der Agent ist verbunden, darf aber noch keinen Ordner öffnen: Fügen Sie unten einen hinzu.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "Der Agent ist noch nicht mit dem Server verbunden: Starten Sie ihn oder verbinden Sie diesen Computer unter Einstellungen.",
"The agent is off.": "Der Agent ist aus.", "The agent is off.": "Der Agent ist aus.",
"The agent is running: agent chats can use this computer.": "Der Agent läuft: Agent-Chats können diesen Computer nutzen.", "The agent is running: agent chats can use this computer.": "Der Agent läuft: Agent-Chats können diesen Computer nutzen.",
"The agent is starting…": "Der Agent startet…", "The agent is starting…": "Der Agent startet…",
"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.": "Der Agent lässt Agent-Chats auf Ihrem Server in den Projekten dieses Computers arbeiten. Er ist das eingebaute LLeMbas CLI und läuft im Hintergrund, solange die App läuft.", "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.": "Der Agent lässt Agent-Chats auf Ihrem Server in den Projekten dieses Computers arbeiten. Er ist das eingebaute LLeMbas CLI und läuft im Hintergrund, solange die App läuft.",
"The agent stopped: %(why)s. Trying again shortly.": "Der Agent hat angehalten: %(why)s. Gleich wird es erneut versucht.", "The agent stopped: %(why)s. Trying again shortly.": "Der Agent hat angehalten: %(why)s. Gleich wird es erneut versucht.",
"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.": "Die App meldet sich einmal an Ihrem Server an, in Ihrem Browser, wie jedes Gerät. Danach kann sie Ihre Chats lesen und schreiben, wie die Weboberfläche, und dieselbe Freigabe verbindet diesen Computer für den Agenten.",
"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.": "Die App meldet sich einmal an Ihrem Server an, in Ihrem Browser, wie jedes Gerät. Danach kann sie Ihre Chats lesen und schreiben, wie die Weboberfläche.",
"The connection to the reply was lost: %(why)s": "Die Verbindung zur Antwort ist abgebrochen: %(why)s",
"The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf", "The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf",
"The reply is ready.": "Die Antwort ist fertig.",
"Thinking": "Denkt nach",
"This computer": "Dieser Computer",
"This computer is not linked to the server yet.": "Dieser Computer ist noch nicht mit dem Server verbunden.", "This computer is not linked to the server yet.": "Dieser Computer ist noch nicht mit dem Server verbunden.",
"This file is too big to show here (%(size)s).": "Diese Datei ist zu groß für die Anzeige (%(size)s).",
"This is not a text file; it cannot be shown here.": "Das ist keine Textdatei; sie kann hier nicht angezeigt werden.",
"Thought": "Nachgedacht",
"Try again": "Erneut versuchen",
"Waiting for the browser…": "Warte auf den Browser…",
"Waiting for the reply before it": "Wartet auf die Antwort davor",
"Waiting for your answer": "Wartet auf Ihre Antwort",
"Writing…": "Schreibt…",
"Your answer": "Ihre Antwort",
"agent off": "Agent aus", "agent off": "Agent aus",
"agent running": "Agent läuft", "agent running": "Agent läuft",
"agent running (service)": "Agent läuft (Dienst)", "agent running (service)": "Agent läuft (Dienst)",
@@ -117,48 +263,121 @@ const LEMBAS_I18N = {
"update %(version)s": "Update %(version)s" "update %(version)s": "Update %(version)s"
}, },
"es": { "es": {
"%(n)s tokens": "tokens: %(n)s",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Ya se está ejecutando aquí un servicio de LLeMbas CLI (pid %(pid)s); es el agente de este ordenador.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Ya se está ejecutando aquí un servicio de LLeMbas CLI (pid %(pid)s); es el agente de este ordenador.",
"A chat asking for more is lowered to this.": "Un chat que pida más se limita a esto.", "A chat asking for more is lowered to this.": "Un chat que pida más se limita a esto.",
"A chat asking for more than the folder allows is lowered to it.": "Un chat que pide más de lo que la carpeta permite se rebaja a ello.",
"A shell as you, in one of these folders.": "Un shell con su usuario, en una de estas carpetas.", "A shell as you, in one of these folders.": "Un shell con su usuario, en una de estas carpetas.",
"About": "Acerca de",
"Add folder…": "Añadir carpeta…", "Add folder…": "Añadir carpeta…",
"Address of your LLeMbas server": "Dirección de su servidor LLeMbas", "Address of your LLeMbas server": "Dirección de su servidor LLeMbas",
"Agent": "Agente", "Agent": "Agente",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Los chats de agente de su servidor pueden trabajar en estas carpetas y en todo lo que contienen — en ningún otro lugar de este ordenador. Inicie uno en la pestaña Agentes del servidor, en «Local».", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Los chats de agente pueden trabajar en estas carpetas y en todo lo que contienen — en ningún otro lugar de este equipo.",
"Agent stopped: %(why)s": "El agente se detuvo: %(why)s", "Agent stopped: %(why)s": "El agente se detuvo: %(why)s",
"Allow": "Permitir",
"Always allow": "Permitir siempre",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Un chat de agente trabaja en una carpeta de este equipo, a través del agente integrado en esta aplicación.",
"Another device": "Otro dispositivo",
"Answer": "Responder",
"Approve this code in the page that opened in your browser:": "Apruebe este código en la página que se abrió en su navegador:",
"Auto — asks nothing": "Auto — no pregunta nada", "Auto — asks nothing": "Auto — no pregunta nada",
"Cancel": "Cancelar",
"Carry it out": "Llevarlo a cabo",
"Chats": "Chats",
"Check for updates…": "Buscar actualizaciones…",
"Close": "Cerrar", "Close": "Cerrar",
"Close (LLeMbas stays in the tray)": "Cerrar (LLeMbas sigue en la bandeja del sistema)", "Close (LLeMbas stays in the tray)": "Cerrar (LLeMbas sigue en la bandeja del sistema)",
"Command": "Comando",
"Connect": "Conectar", "Connect": "Conectar",
"Copy": "Copiar",
"Cut short here; the whole of it is in the web interface.": "Recortado aquí; completo está en la interfaz web.",
"Deny": "Denegar",
"Earlier messages": "Mensajes anteriores",
"Edit — asks before commands": "Edit — pregunta antes de los comandos", "Edit — asks before commands": "Edit — pregunta antes de los comandos",
"Empty folder": "Carpeta vacía",
"Folder": "Carpeta",
"Install LLeMbas %(version)s…": "Instalar LLeMbas %(version)s…",
"LLeMbas %(version)s is available (this is %(current)s)": "LLeMbas %(version)s está disponible (esta es la %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "LLeMbas %(version)s está disponible (esta es la %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop necesita un servidor LLeMbas 2.0 o posterior.",
"Let the server open a terminal here": "Permitir que el servidor abra un terminal aquí", "Let the server open a terminal here": "Permitir que el servidor abra un terminal aquí",
"Link again": "Vincular de nuevo", "Link again": "Vincular de nuevo",
"Link this computer": "Vincular este ordenador", "Link this computer": "Vincular este ordenador",
"Loading…": "Cargando…",
"Looking for a LLeMbas server there…": "Buscando un servidor LLeMbas allí…", "Looking for a LLeMbas server there…": "Buscando un servidor LLeMbas allí…",
"Manual — asks first": "Manual — pregunta primero", "Manual — asks first": "Manual — pregunta primero",
"Maximise": "Maximizar", "Maximise": "Maximizar",
"Menu": "Menú", "Menu": "Menú",
"Minimise": "Minimizar", "Minimise": "Minimizar",
"Mode": "Modo",
"Model": "Modelo",
"More": "Más",
"New agent chat": "Nuevo chat de agente",
"New chat": "Nuevo chat",
"No agent chat yet. Start one: it works in a folder on this computer.": "Aún no hay chats de agente. Inicie uno: trabaja en una carpeta de este equipo.",
"No chat yet.": "Aún no hay chats.",
"No folder yet: add one under Settings, Project folders.": "Aún no hay carpeta: añada una en Ajustes, Carpetas de proyecto.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Aún no hay ninguna carpeta: el agente no puede abrir nada. Añada la carpeta de un proyecto.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Aún no hay ninguna carpeta: el agente no puede abrir nada. Añada la carpeta de un proyecto.",
"Open the server": "Abrir el servidor", "No model is available to this account.": "Ningún modelo está disponible para esta cuenta.",
"No project folder yet: the agent can open nothing on this computer.": "Aún no hay carpeta de proyecto: el agente no puede abrir nada en este equipo.",
"Not yet": "Todavía no",
"Notifications": "Notificaciones",
"Only the agent's folders: Settings, Project folders.": "Solo las carpetas del agente: Ajustes, Carpetas de proyecto.",
"Open in the viewer": "Abrir en el visor",
"Open the page again": "Abrir la página de nuevo",
"Or type an answer": "O escriba una respuesta",
"Other devices": "Otros dispositivos",
"Page": "Página",
"Plan — changes nothing": "Plan — no cambia nada", "Plan — changes nothing": "Plan — no cambia nada",
"Preview": "Vista previa",
"Project folders": "Carpetas de proyectos", "Project folders": "Carpetas de proyectos",
"Read again": "Leer de nuevo",
"Recommended": "Recomendado",
"Remove": "Quitar", "Remove": "Quitar",
"Remove %(dir)s": "Quitar %(dir)s", "Remove %(dir)s": "Quitar %(dir)s",
"Restore": "Restaurar", "Restore": "Restaurar",
"Send": "Enviar",
"Server": "Servidor", "Server": "Servidor",
"Sign in there as you would in a browser.": "Inicie sesión allí como lo haría en un navegador.", "Settings": "Ajustes",
"Shown by the agent": "Mostrado por el agente",
"Sign in": "Iniciar sesión",
"Sign out": "Cerrar sesión",
"Signed in as %(who)s.": "Sesión iniciada como %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Al cerrar sesión, la aplicación renuncia a su acceso en el servidor; la vinculación del agente se mantiene. Cierre sesión para usar otro servidor.",
"Source": "Código fuente",
"Start the agent": "Iniciar el agente", "Start the agent": "Iniciar el agente",
"Stop the agent": "Detener el agente", "Stop the agent": "Detener el agente",
"Stopped.": "Detenido.",
"Taken in while it ran:": "Incorporado mientras se ejecutaba:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Avisarme cuando una respuesta o el trabajo de un agente termine mientras estoy en otra parte",
"Terminal": "Terminal", "Terminal": "Terminal",
"The agent asks to run %(tool)s:": "El agente pide ejecutar %(tool)s:",
"The agent asks:": "El agente pregunta:",
"The agent has finished.": "El agente ha terminado.",
"The agent is connected, but may open no folder yet: add one below.": "El agente está conectado, pero aún no puede abrir ninguna carpeta: añada una abajo.", "The agent is connected, but may open no folder yet: add one below.": "El agente está conectado, pero aún no puede abrir ninguna carpeta: añada una abajo.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "El agente aún no está conectado al servidor: inícielo o vincule este equipo en Ajustes.",
"The agent is off.": "El agente está apagado.", "The agent is off.": "El agente está apagado.",
"The agent is running: agent chats can use this computer.": "El agente está en marcha: los chats de agente pueden usar este ordenador.", "The agent is running: agent chats can use this computer.": "El agente está en marcha: los chats de agente pueden usar este ordenador.",
"The agent is starting…": "El agente se está iniciando…", "The agent is starting…": "El agente se está iniciando…",
"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.": "El agente permite que los chats de agente de su servidor trabajen en los proyectos de este ordenador. Es el LLeMbas CLI, integrado, que se ejecuta en segundo plano mientras la aplicación está abierta.", "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.": "El agente permite que los chats de agente de su servidor trabajen en los proyectos de este ordenador. Es el LLeMbas CLI, integrado, que se ejecuta en segundo plano mientras la aplicación está abierta.",
"The agent stopped: %(why)s. Trying again shortly.": "El agente se detuvo: %(why)s. Se volverá a intentar en breve.", "The agent stopped: %(why)s. Trying again shortly.": "El agente se detuvo: %(why)s. Se volverá a intentar en breve.",
"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.": "La aplicación inicia sesión en su servidor una vez, en su navegador, como cualquier dispositivo. Después puede leer y escribir sus chats, como la interfaz web, y la misma aprobación vincula este ordenador para el agente.",
"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.": "La aplicación inicia sesión en su servidor una vez, en su navegador, como cualquier dispositivo. Después puede leer y escribir sus chats, como la interfaz web.",
"The connection to the reply was lost: %(why)s": "Se perdió la conexión con la respuesta: %(why)s",
"The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí", "The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí",
"The reply is ready.": "La respuesta está lista.",
"Thinking": "Pensando",
"This computer": "Este equipo",
"This computer is not linked to the server yet.": "Este ordenador aún no está vinculado al servidor.", "This computer is not linked to the server yet.": "Este ordenador aún no está vinculado al servidor.",
"This file is too big to show here (%(size)s).": "Este archivo es demasiado grande para mostrarlo (%(size)s).",
"This is not a text file; it cannot be shown here.": "No es un archivo de texto; no se puede mostrar aquí.",
"Thought": "Pensamiento",
"Try again": "Reintentar",
"Waiting for the browser…": "Esperando al navegador…",
"Waiting for the reply before it": "Esperando la respuesta anterior",
"Waiting for your answer": "Esperando su respuesta",
"Writing…": "Escribiendo…",
"Your answer": "Su respuesta",
"agent off": "agente apagado", "agent off": "agente apagado",
"agent running": "agente en marcha", "agent running": "agente en marcha",
"agent running (service)": "agente en marcha (servicio)", "agent running (service)": "agente en marcha (servicio)",
@@ -170,48 +389,121 @@ const LEMBAS_I18N = {
"update %(version)s": "actualizar a %(version)s" "update %(version)s": "actualizar a %(version)s"
}, },
"fr": { "fr": {
"%(n)s tokens": "%(n)s jetons",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Un service LLeMbas CLI tourne déjà ici (pid %(pid)s) ; c'est l'agent de cet ordinateur.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Un service LLeMbas CLI tourne déjà ici (pid %(pid)s) ; c'est l'agent de cet ordinateur.",
"A chat asking for more is lowered to this.": "Une conversation qui en demande plus est ramenée à ceci.", "A chat asking for more is lowered to this.": "Une conversation qui en demande plus est ramenée à ceci.",
"A chat asking for more than the folder allows is lowered to it.": "Une discussion qui demande plus que le dossier ne permet y est ramenée.",
"A shell as you, in one of these folders.": "Un shell en votre nom, dans l'un de ces dossiers.", "A shell as you, in one of these folders.": "Un shell en votre nom, dans l'un de ces dossiers.",
"About": "À propos",
"Add folder…": "Ajouter un dossier…", "Add folder…": "Ajouter un dossier…",
"Address of your LLeMbas server": "Adresse de votre serveur LLeMbas", "Address of your LLeMbas server": "Adresse de votre serveur LLeMbas",
"Agent": "Agent", "Agent": "Agent",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Les conversations d'agent de votre serveur peuvent travailler dans ces dossiers et tout ce qu'ils contiennent — nulle part ailleurs sur cet ordinateur. Démarrez-en une dans l'onglet Agents du serveur, sur « Local ».", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Les discussions d'agent peuvent travailler dans ces dossiers et tout ce qu'ils contiennent — nulle part ailleurs sur cet ordinateur.",
"Agent stopped: %(why)s": "L'agent s'est arrêté : %(why)s", "Agent stopped: %(why)s": "L'agent s'est arrêté : %(why)s",
"Allow": "Autoriser",
"Always allow": "Toujours autoriser",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Une discussion d'agent travaille dans un dossier de cet ordinateur, via l'agent intégré à cette application.",
"Another device": "Un autre appareil",
"Answer": "Répondre",
"Approve this code in the page that opened in your browser:": "Approuvez ce code sur la page qui s'est ouverte dans votre navigateur :",
"Auto — asks nothing": "Auto — ne demande rien", "Auto — asks nothing": "Auto — ne demande rien",
"Cancel": "Annuler",
"Carry it out": "Exécuter",
"Chats": "Conversations",
"Check for updates…": "Rechercher des mises à jour…",
"Close": "Fermer", "Close": "Fermer",
"Close (LLeMbas stays in the tray)": "Fermer (LLeMbas reste dans la zone de notification)", "Close (LLeMbas stays in the tray)": "Fermer (LLeMbas reste dans la zone de notification)",
"Command": "Commande",
"Connect": "Se connecter", "Connect": "Se connecter",
"Copy": "Copier",
"Cut short here; the whole of it is in the web interface.": "Tronqué ici ; le tout est dans l'interface web.",
"Deny": "Refuser",
"Earlier messages": "Messages précédents",
"Edit — asks before commands": "Edit — demande avant les commandes", "Edit — asks before commands": "Edit — demande avant les commandes",
"Empty folder": "Dossier vide",
"Folder": "Dossier",
"Install LLeMbas %(version)s…": "Installer LLeMbas %(version)s…",
"LLeMbas %(version)s is available (this is %(current)s)": "LLeMbas %(version)s est disponible (ceci est la %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "LLeMbas %(version)s est disponible (ceci est la %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop nécessite un serveur LLeMbas 2.0 ou plus récent.",
"Let the server open a terminal here": "Laisser le serveur ouvrir un terminal ici", "Let the server open a terminal here": "Laisser le serveur ouvrir un terminal ici",
"Link again": "Relier à nouveau", "Link again": "Relier à nouveau",
"Link this computer": "Relier cet ordinateur", "Link this computer": "Relier cet ordinateur",
"Loading…": "Chargement…",
"Looking for a LLeMbas server there…": "Recherche d'un serveur LLeMbas à cette adresse…", "Looking for a LLeMbas server there…": "Recherche d'un serveur LLeMbas à cette adresse…",
"Manual — asks first": "Manual — demande d'abord", "Manual — asks first": "Manual — demande d'abord",
"Maximise": "Agrandir", "Maximise": "Agrandir",
"Menu": "Menu", "Menu": "Menu",
"Minimise": "Réduire", "Minimise": "Réduire",
"Mode": "Mode",
"Model": "Modèle",
"More": "Plus",
"New agent chat": "Nouvelle discussion d'agent",
"New chat": "Nouvelle conversation",
"No agent chat yet. Start one: it works in a folder on this computer.": "Aucune discussion d'agent pour l'instant. Commencez-en une : elle travaille dans un dossier de cet ordinateur.",
"No chat yet.": "Aucune discussion pour l'instant.",
"No folder yet: add one under Settings, Project folders.": "Aucun dossier pour l'instant : ajoutez-en un dans Paramètres, Dossiers de projet.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Aucun dossier pour l'instant : l'agent ne peut rien ouvrir. Ajoutez le dossier d'un projet.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Aucun dossier pour l'instant : l'agent ne peut rien ouvrir. Ajoutez le dossier d'un projet.",
"Open the server": "Ouvrir le serveur", "No model is available to this account.": "Aucun modèle n'est disponible pour ce compte.",
"No project folder yet: the agent can open nothing on this computer.": "Aucun dossier de projet pour l'instant : l'agent ne peut rien ouvrir sur cet ordinateur.",
"Not yet": "Pas encore",
"Notifications": "Notifications",
"Only the agent's folders: Settings, Project folders.": "Uniquement les dossiers de l'agent : Paramètres, Dossiers de projet.",
"Open in the viewer": "Ouvrir dans la visionneuse",
"Open the page again": "Rouvrir la page",
"Or type an answer": "Ou saisissez une réponse",
"Other devices": "Autres appareils",
"Page": "Page",
"Plan — changes nothing": "Plan — ne change rien", "Plan — changes nothing": "Plan — ne change rien",
"Preview": "Aperçu",
"Project folders": "Dossiers de projets", "Project folders": "Dossiers de projets",
"Read again": "Relire",
"Recommended": "Recommandé",
"Remove": "Retirer", "Remove": "Retirer",
"Remove %(dir)s": "Retirer %(dir)s", "Remove %(dir)s": "Retirer %(dir)s",
"Restore": "Restaurer", "Restore": "Restaurer",
"Send": "Envoyer",
"Server": "Serveur", "Server": "Serveur",
"Sign in there as you would in a browser.": "Connectez-vous là-bas comme dans un navigateur.", "Settings": "Paramètres",
"Shown by the agent": "Montré par l'agent",
"Sign in": "Se connecter",
"Sign out": "Se déconnecter",
"Signed in as %(who)s.": "Connecté en tant que %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "La déconnexion abandonne la connexion de l'application au serveur ; la liaison de l'agent reste. Déconnectez-vous pour utiliser un autre serveur.",
"Source": "Source",
"Start the agent": "Démarrer l'agent", "Start the agent": "Démarrer l'agent",
"Stop the agent": "Arrêter l'agent", "Stop the agent": "Arrêter l'agent",
"Stopped.": "Arrêté.",
"Taken in while it ran:": "Pris en compte pendant l'exécution :",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Me prévenir quand une réponse ou le travail d'un agent est terminé pendant que je suis ailleurs",
"Terminal": "Terminal", "Terminal": "Terminal",
"The agent asks to run %(tool)s:": "L'agent demande à exécuter %(tool)s :",
"The agent asks:": "L'agent demande :",
"The agent has finished.": "L'agent a terminé.",
"The agent is connected, but may open no folder yet: add one below.": "L'agent est connecté, mais ne peut encore ouvrir aucun dossier : ajoutez-en un ci-dessous.", "The agent is connected, but may open no folder yet: add one below.": "L'agent est connecté, mais ne peut encore ouvrir aucun dossier : ajoutez-en un ci-dessous.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "L'agent n'est pas encore connecté au serveur : démarrez-le ou reliez cet ordinateur dans les Paramètres.",
"The agent is off.": "L'agent est arrêté.", "The agent is off.": "L'agent est arrêté.",
"The agent is running: agent chats can use this computer.": "L'agent tourne : les conversations d'agent peuvent utiliser cet ordinateur.", "The agent is running: agent chats can use this computer.": "L'agent tourne : les conversations d'agent peuvent utiliser cet ordinateur.",
"The agent is starting…": "L'agent démarre…", "The agent is starting…": "L'agent démarre…",
"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.": "L'agent permet aux conversations d'agent de votre serveur de travailler dans les projets de cet ordinateur. C'est le LLeMbas CLI, intégré, qui tourne en arrière-plan tant que l'application est ouverte.", "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.": "L'agent permet aux conversations d'agent de votre serveur de travailler dans les projets de cet ordinateur. C'est le LLeMbas CLI, intégré, qui tourne en arrière-plan tant que l'application est ouverte.",
"The agent stopped: %(why)s. Trying again shortly.": "L'agent s'est arrêté : %(why)s. Nouvel essai sous peu.", "The agent stopped: %(why)s. Trying again shortly.": "L'agent s'est arrêté : %(why)s. Nouvel essai sous peu.",
"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.": "L'application se connecte une fois à votre serveur, dans votre navigateur, comme tout appareil. Elle peut ensuite lire et écrire vos discussions, comme l'interface web, et la même approbation relie cet ordinateur pour l'agent.",
"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.": "L'application se connecte une fois à votre serveur, dans votre navigateur, comme tout appareil. Elle peut ensuite lire et écrire vos discussions, comme l'interface web.",
"The connection to the reply was lost: %(why)s": "La connexion à la réponse a été perdue : %(why)s",
"The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici", "The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici",
"The reply is ready.": "La réponse est prête.",
"Thinking": "Réflexion",
"This computer": "Cet ordinateur",
"This computer is not linked to the server yet.": "Cet ordinateur n'est pas encore relié au serveur.", "This computer is not linked to the server yet.": "Cet ordinateur n'est pas encore relié au serveur.",
"This file is too big to show here (%(size)s).": "Ce fichier est trop volumineux pour être affiché (%(size)s).",
"This is not a text file; it cannot be shown here.": "Ce n'est pas un fichier texte ; il ne peut pas être affiché ici.",
"Thought": "Réflexion",
"Try again": "Réessayer",
"Waiting for the browser…": "En attente du navigateur…",
"Waiting for the reply before it": "En attente de la réponse précédente",
"Waiting for your answer": "En attente de votre réponse",
"Writing…": "Rédaction…",
"Your answer": "Votre réponse",
"agent off": "agent arrêté", "agent off": "agent arrêté",
"agent running": "agent actif", "agent running": "agent actif",
"agent running (service)": "agent actif (service)", "agent running (service)": "agent actif (service)",
@@ -223,48 +515,121 @@ const LEMBAS_I18N = {
"update %(version)s": "mise à jour %(version)s" "update %(version)s": "mise à jour %(version)s"
}, },
"hu": { "hu": {
"%(n)s tokens": "%(n)s token",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Itt már fut egy LLeMbas CLI szolgáltatás (pid %(pid)s); ez a számítógép ügynöke.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Itt már fut egy LLeMbas CLI szolgáltatás (pid %(pid)s); ez a számítógép ügynöke.",
"A chat asking for more is lowered to this.": "Az ennél többet kérő csevegés erre korlátozódik.", "A chat asking for more is lowered to this.": "Az ennél többet kérő csevegés erre korlátozódik.",
"A chat asking for more than the folder allows is lowered to it.": "A csevegés, amely többet kér, mint amit a mappa enged, erre lesz csökkentve.",
"A shell as you, in one of these folders.": "Egy shell az Ön nevében, ezek egyik mappájában.", "A shell as you, in one of these folders.": "Egy shell az Ön nevében, ezek egyik mappájában.",
"About": "Névjegy",
"Add folder…": "Mappa hozzáadása…", "Add folder…": "Mappa hozzáadása…",
"Address of your LLeMbas server": "A LLeMbas-kiszolgáló címe", "Address of your LLeMbas server": "A LLeMbas-kiszolgáló címe",
"Agent": "Ügynök", "Agent": "Ügynök",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "A kiszolgáló ügynök-csevegései ezekben a mappákban és a bennük lévőkben dolgozhatnak — a számítógépen máshol nem. Indítson egyet a kiszolgáló Ügynökök lapján, a „Helyi” eszközön.", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Az ügynökcsevegések ezekben a mappákban és mindenben, ami bennük van, dolgozhatnak — sehol máshol ezen a számítógépen.",
"Agent stopped: %(why)s": "Az ügynök leállt: %(why)s", "Agent stopped: %(why)s": "Az ügynök leállt: %(why)s",
"Allow": "Engedélyezés",
"Always allow": "Mindig engedélyez",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Az ügynökcsevegés egy mappában dolgozik ezen a számítógépen, az alkalmazásba épített ügynökön keresztül.",
"Another device": "Másik eszköz",
"Answer": "Válasz",
"Approve this code in the page that opened in your browser:": "Hagyja jóvá ezt a kódot a böngészőjében megnyílt oldalon:",
"Auto — asks nothing": "Auto — semmit nem kérdez", "Auto — asks nothing": "Auto — semmit nem kérdez",
"Cancel": "Mégse",
"Carry it out": "Végrehajtás",
"Chats": "Csevegések",
"Check for updates…": "Frissítések keresése…",
"Close": "Bezárás", "Close": "Bezárás",
"Close (LLeMbas stays in the tray)": "Bezárás (a LLeMbas a tálcán marad)", "Close (LLeMbas stays in the tray)": "Bezárás (a LLeMbas a tálcán marad)",
"Command": "Parancs",
"Connect": "Csatlakozás", "Connect": "Csatlakozás",
"Copy": "Másolás",
"Cut short here; the whole of it is in the web interface.": "Itt rövidítve; a teljes a webes felületen van.",
"Deny": "Elutasítás",
"Earlier messages": "Korábbi üzenetek",
"Edit — asks before commands": "Edit — parancsok előtt kérdez", "Edit — asks before commands": "Edit — parancsok előtt kérdez",
"Empty folder": "Üres mappa",
"Folder": "Mappa",
"Install LLeMbas %(version)s…": "LLeMbas %(version)s telepítése…",
"LLeMbas %(version)s is available (this is %(current)s)": "Elérhető a LLeMbas %(version)s (ez a %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "Elérhető a LLeMbas %(version)s (ez a %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "A LLeMbas Desktophoz LLeMbas 2.0 vagy újabb szerver kell.",
"Let the server open a terminal here": "A kiszolgáló nyithasson itt terminált", "Let the server open a terminal here": "A kiszolgáló nyithasson itt terminált",
"Link again": "Újra összekapcsolás", "Link again": "Újra összekapcsolás",
"Link this computer": "A számítógép összekapcsolása", "Link this computer": "A számítógép összekapcsolása",
"Loading…": "Betöltés…",
"Looking for a LLeMbas server there…": "LLeMbas-kiszolgáló keresése ott…", "Looking for a LLeMbas server there…": "LLeMbas-kiszolgáló keresése ott…",
"Manual — asks first": "Manual — előbb kérdez", "Manual — asks first": "Manual — előbb kérdez",
"Maximise": "Teljes méret", "Maximise": "Teljes méret",
"Menu": "Menü", "Menu": "Menü",
"Minimise": "Kis méret", "Minimise": "Kis méret",
"Mode": "Mód",
"Model": "Modell",
"More": "Több",
"New agent chat": "Új ügynökcsevegés",
"New chat": "Új csevegés",
"No agent chat yet. Start one: it works in a folder on this computer.": "Még nincs ügynökcsevegés. Indítson egyet: egy mappában dolgozik ezen a számítógépen.",
"No chat yet.": "Még nincs csevegés.",
"No folder yet: add one under Settings, Project folders.": "Még nincs mappa: adjon hozzá egyet a Beállítások, Projektmappák alatt.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Még nincs mappa: az ügynök semmit nem nyithat meg. Adja hozzá egy projekt mappáját.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Még nincs mappa: az ügynök semmit nem nyithat meg. Adja hozzá egy projekt mappáját.",
"Open the server": "Kiszolgáló megnyitása", "No model is available to this account.": "Ennek a fióknak nem érhető el modell.",
"No project folder yet: the agent can open nothing on this computer.": "Még nincs projektmappa: az ügynök semmit sem nyithat meg ezen a számítógépen.",
"Not yet": "Még nem",
"Notifications": "Értesítések",
"Only the agent's folders: Settings, Project folders.": "Csak az ügynök mappái: Beállítások, Projektmappák.",
"Open in the viewer": "Megnyitás a megjelenítőben",
"Open the page again": "Oldal újbóli megnyitása",
"Or type an answer": "Vagy írjon választ",
"Other devices": "Más eszközök",
"Page": "Oldal",
"Plan — changes nothing": "Plan — semmit nem változtat", "Plan — changes nothing": "Plan — semmit nem változtat",
"Preview": "Előnézet",
"Project folders": "Projektmappák", "Project folders": "Projektmappák",
"Read again": "Újraolvasás",
"Recommended": "Ajánlott",
"Remove": "Eltávolítás", "Remove": "Eltávolítás",
"Remove %(dir)s": "%(dir)s eltávolítása", "Remove %(dir)s": "%(dir)s eltávolítása",
"Restore": "Visszaállítás", "Restore": "Visszaállítás",
"Send": "Küldés",
"Server": "Kiszolgáló", "Server": "Kiszolgáló",
"Sign in there as you would in a browser.": "Jelentkezzen be ott úgy, mint egy böngészőben.", "Settings": "Beállítások",
"Shown by the agent": "Az ügynök mutatta",
"Sign in": "Bejelentkezés",
"Sign out": "Kijelentkezés",
"Signed in as %(who)s.": "Bejelentkezve: %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Kijelentkezéskor az alkalmazás lemond a szerveren lévő bejelentkezéséről; az ügynök kapcsolata megmarad. Más szerver használatához jelentkezzen ki.",
"Source": "Forrás",
"Start the agent": "Ügynök indítása", "Start the agent": "Ügynök indítása",
"Stop the agent": "Ügynök leállítása", "Stop the agent": "Ügynök leállítása",
"Stopped.": "Leállítva.",
"Taken in while it ran:": "Futás közben átvéve:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Szóljon, ha egy válasz vagy az ügynök munkája elkészül, amíg máshol vagyok",
"Terminal": "Terminál", "Terminal": "Terminál",
"The agent asks to run %(tool)s:": "Az ügynök a(z) %(tool)s futtatását kéri:",
"The agent asks:": "Az ügynök kérdezi:",
"The agent has finished.": "Az ügynök befejezte.",
"The agent is connected, but may open no folder yet: add one below.": "Az ügynök csatlakozott, de még egy mappát sem nyithat meg: adjon hozzá egyet lent.", "The agent is connected, but may open no folder yet: add one below.": "Az ügynök csatlakozott, de még egy mappát sem nyithat meg: adjon hozzá egyet lent.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "Az ügynök még nem kapcsolódik a szerverhez: indítsa el, vagy kapcsolja össze ezt a számítógépet a Beállításokban.",
"The agent is off.": "Az ügynök ki van kapcsolva.", "The agent is off.": "Az ügynök ki van kapcsolva.",
"The agent is running: agent chats can use this computer.": "Az ügynök fut: az ügynök-csevegések használhatják ezt a számítógépet.", "The agent is running: agent chats can use this computer.": "Az ügynök fut: az ügynök-csevegések használhatják ezt a számítógépet.",
"The agent is starting…": "Az ügynök indul…", "The agent is starting…": "Az ügynök indul…",
"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.": "Az ügynök lehetővé teszi, hogy a kiszolgálón futó ügynök-csevegések ennek a számítógépnek a projektjeiben dolgozzanak. Ez a beépített LLeMbas CLI, amely a háttérben fut, amíg az alkalmazás fut.", "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.": "Az ügynök lehetővé teszi, hogy a kiszolgálón futó ügynök-csevegések ennek a számítógépnek a projektjeiben dolgozzanak. Ez a beépített LLeMbas CLI, amely a háttérben fut, amíg az alkalmazás fut.",
"The agent stopped: %(why)s. Trying again shortly.": "Az ügynök leállt: %(why)s. Hamarosan újra próbálja.", "The agent stopped: %(why)s. Trying again shortly.": "Az ügynök leállt: %(why)s. Hamarosan újra próbálja.",
"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.": "Az alkalmazás egyszer jelentkezik be a szerverére, a böngészőjében, mint bármely eszköz. Ezután olvashatja és írhatja a csevegéseit, mint a webes felület, és ugyanez a jóváhagyás összekapcsolja ezt a számítógépet az ügynök számára.",
"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.": "Az alkalmazás egyszer jelentkezik be a szerverére, a böngészőjében, mint bármely eszköz. Ezután olvashatja és írhatja a csevegéseit, mint a webes felület.",
"The connection to the reply was lost: %(why)s": "A kapcsolat a válasszal megszakadt: %(why)s",
"The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet", "The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet",
"The reply is ready.": "A válasz kész.",
"Thinking": "Gondolkodás",
"This computer": "Ez a számítógép",
"This computer is not linked to the server yet.": "Ez a számítógép még nincs összekapcsolva a kiszolgálóval.", "This computer is not linked to the server yet.": "Ez a számítógép még nincs összekapcsolva a kiszolgálóval.",
"This file is too big to show here (%(size)s).": "Ez a fájl túl nagy a megjelenítéshez (%(size)s).",
"This is not a text file; it cannot be shown here.": "Ez nem szövegfájl; itt nem jeleníthető meg.",
"Thought": "Gondolkodott",
"Try again": "Újrapróbálás",
"Waiting for the browser…": "Várakozás a böngészőre…",
"Waiting for the reply before it": "Az előző válaszra vár",
"Waiting for your answer": "Az Ön válaszára vár",
"Writing…": "Írás…",
"Your answer": "Az Ön válasza",
"agent off": "ügynök kikapcsolva", "agent off": "ügynök kikapcsolva",
"agent running": "ügynök fut", "agent running": "ügynök fut",
"agent running (service)": "ügynök fut (szolgáltatás)", "agent running (service)": "ügynök fut (szolgáltatás)",
@@ -276,48 +641,121 @@ const LEMBAS_I18N = {
"update %(version)s": "frissítés: %(version)s" "update %(version)s": "frissítés: %(version)s"
}, },
"it": { "it": {
"%(n)s tokens": "token: %(n)s",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Un servizio LLeMbas CLI è già in esecuzione qui (pid %(pid)s); è l'agente di questo computer.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Un servizio LLeMbas CLI è già in esecuzione qui (pid %(pid)s); è l'agente di questo computer.",
"A chat asking for more is lowered to this.": "Una chat che chiede di più viene limitata a questo.", "A chat asking for more is lowered to this.": "Una chat che chiede di più viene limitata a questo.",
"A chat asking for more than the folder allows is lowered to it.": "Una chat che chiede più di quanto la cartella consente viene ridotta a quello.",
"A shell as you, in one of these folders.": "Una shell con il tuo utente, in una di queste cartelle.", "A shell as you, in one of these folders.": "Una shell con il tuo utente, in una di queste cartelle.",
"About": "Informazioni",
"Add folder…": "Aggiungi cartella…", "Add folder…": "Aggiungi cartella…",
"Address of your LLeMbas server": "Indirizzo del tuo server LLeMbas", "Address of your LLeMbas server": "Indirizzo del tuo server LLeMbas",
"Agent": "Agente", "Agent": "Agente",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Le chat agente del tuo server possono lavorare in queste cartelle e in tutto ciò che contengono — in nessun altro posto di questo computer. Avviane una nella scheda Agenti del server, su “Locale”.", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Le chat con agente possono lavorare in queste cartelle e in tutto ciò che contengono — in nessun altro punto di questo computer.",
"Agent stopped: %(why)s": "L'agente si è fermato: %(why)s", "Agent stopped: %(why)s": "L'agente si è fermato: %(why)s",
"Allow": "Consenti",
"Always allow": "Consenti sempre",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Una chat con agente lavora in una cartella di questo computer, tramite l'agente integrato in questa app.",
"Another device": "Un altro dispositivo",
"Answer": "Rispondi",
"Approve this code in the page that opened in your browser:": "Approvi questo codice nella pagina che si è aperta nel suo browser:",
"Auto — asks nothing": "Auto — non chiede nulla", "Auto — asks nothing": "Auto — non chiede nulla",
"Cancel": "Annulla",
"Carry it out": "Eseguilo",
"Chats": "Chat",
"Check for updates…": "Cerca aggiornamenti…",
"Close": "Chiudi", "Close": "Chiudi",
"Close (LLeMbas stays in the tray)": "Chiudi (LLeMbas resta nell'area di notifica)", "Close (LLeMbas stays in the tray)": "Chiudi (LLeMbas resta nell'area di notifica)",
"Command": "Comando",
"Connect": "Connetti", "Connect": "Connetti",
"Copy": "Copia",
"Cut short here; the whole of it is in the web interface.": "Troncato qui; il resto è nell'interfaccia web.",
"Deny": "Nega",
"Earlier messages": "Messaggi precedenti",
"Edit — asks before commands": "Edit — chiede prima dei comandi", "Edit — asks before commands": "Edit — chiede prima dei comandi",
"Empty folder": "Cartella vuota",
"Folder": "Cartella",
"Install LLeMbas %(version)s…": "Installa LLeMbas %(version)s…",
"LLeMbas %(version)s is available (this is %(current)s)": "È disponibile LLeMbas %(version)s (questa è la %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "È disponibile LLeMbas %(version)s (questa è la %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop richiede un server LLeMbas 2.0 o successivo.",
"Let the server open a terminal here": "Lascia che il server apra un terminale qui", "Let the server open a terminal here": "Lascia che il server apra un terminale qui",
"Link again": "Collega di nuovo", "Link again": "Collega di nuovo",
"Link this computer": "Collega questo computer", "Link this computer": "Collega questo computer",
"Loading…": "Caricamento…",
"Looking for a LLeMbas server there…": "Cerco un server LLeMbas lì…", "Looking for a LLeMbas server there…": "Cerco un server LLeMbas lì…",
"Manual — asks first": "Manual — chiede prima", "Manual — asks first": "Manual — chiede prima",
"Maximise": "Ingrandisci", "Maximise": "Ingrandisci",
"Menu": "Menu", "Menu": "Menu",
"Minimise": "Riduci a icona", "Minimise": "Riduci a icona",
"Mode": "Modalità",
"Model": "Modello",
"More": "Altro",
"New agent chat": "Nuova chat con agente",
"New chat": "Nuova chat",
"No agent chat yet. Start one: it works in a folder on this computer.": "Nessuna chat con agente ancora. Ne inizi una: lavora in una cartella di questo computer.",
"No chat yet.": "Nessuna chat ancora.",
"No folder yet: add one under Settings, Project folders.": "Nessuna cartella ancora: ne aggiunga una in Impostazioni, Cartelle di progetto.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Ancora nessuna cartella: l'agente non può aprire nulla. Aggiungi la cartella di un progetto.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Ancora nessuna cartella: l'agente non può aprire nulla. Aggiungi la cartella di un progetto.",
"Open the server": "Apri il server", "No model is available to this account.": "Nessun modello è disponibile per questo account.",
"No project folder yet: the agent can open nothing on this computer.": "Nessuna cartella di progetto ancora: l'agente non può aprire nulla su questo computer.",
"Not yet": "Non ancora",
"Notifications": "Notifiche",
"Only the agent's folders: Settings, Project folders.": "Solo le cartelle dell'agente: Impostazioni, Cartelle di progetto.",
"Open in the viewer": "Apri nel visualizzatore",
"Open the page again": "Riapri la pagina",
"Or type an answer": "Oppure scriva una risposta",
"Other devices": "Altri dispositivi",
"Page": "Pagina",
"Plan — changes nothing": "Plan — non cambia nulla", "Plan — changes nothing": "Plan — non cambia nulla",
"Preview": "Anteprima",
"Project folders": "Cartelle dei progetti", "Project folders": "Cartelle dei progetti",
"Read again": "Rileggi",
"Recommended": "Consigliato",
"Remove": "Rimuovi", "Remove": "Rimuovi",
"Remove %(dir)s": "Rimuovi %(dir)s", "Remove %(dir)s": "Rimuovi %(dir)s",
"Restore": "Ripristina", "Restore": "Ripristina",
"Send": "Invia",
"Server": "Server", "Server": "Server",
"Sign in there as you would in a browser.": "Accedi lì come faresti in un browser.", "Settings": "Impostazioni",
"Shown by the agent": "Mostrato dall'agente",
"Sign in": "Accedi",
"Sign out": "Esci",
"Signed in as %(who)s.": "Accesso effettuato come %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Uscendo, l'app rinuncia al suo accesso sul server; il collegamento dell'agente resta. Esca per usare un altro server.",
"Source": "Sorgente",
"Start the agent": "Avvia l'agente", "Start the agent": "Avvia l'agente",
"Stop the agent": "Ferma l'agente", "Stop the agent": "Ferma l'agente",
"Stopped.": "Interrotto.",
"Taken in while it ran:": "Accolto durante l'esecuzione:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Avvisami quando una risposta o il lavoro di un agente è finito mentre sono altrove",
"Terminal": "Terminale", "Terminal": "Terminale",
"The agent asks to run %(tool)s:": "L'agente chiede di eseguire %(tool)s:",
"The agent asks:": "L'agente chiede:",
"The agent has finished.": "L'agente ha finito.",
"The agent is connected, but may open no folder yet: add one below.": "L'agente è connesso, ma non può ancora aprire nessuna cartella: aggiungine una qui sotto.", "The agent is connected, but may open no folder yet: add one below.": "L'agente è connesso, ma non può ancora aprire nessuna cartella: aggiungine una qui sotto.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "L'agente non è ancora connesso al server: lo avvii o colleghi questo computer nelle Impostazioni.",
"The agent is off.": "L'agente è spento.", "The agent is off.": "L'agente è spento.",
"The agent is running: agent chats can use this computer.": "L'agente è in esecuzione: le chat agente possono usare questo computer.", "The agent is running: agent chats can use this computer.": "L'agente è in esecuzione: le chat agente possono usare questo computer.",
"The agent is starting…": "L'agente si sta avviando…", "The agent is starting…": "L'agente si sta avviando…",
"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.": "L'agente permette alle chat agente del tuo server di lavorare nei progetti di questo computer. È il LLeMbas CLI, integrato, in esecuzione in background finché l'app è aperta.", "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.": "L'agente permette alle chat agente del tuo server di lavorare nei progetti di questo computer. È il LLeMbas CLI, integrato, in esecuzione in background finché l'app è aperta.",
"The agent stopped: %(why)s. Trying again shortly.": "L'agente si è fermato: %(why)s. Tra poco riprova.", "The agent stopped: %(why)s. Trying again shortly.": "L'agente si è fermato: %(why)s. Tra poco riprova.",
"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.": "L'app accede al suo server una volta, nel suo browser, come ogni dispositivo. Poi può leggere e scrivere le sue chat, come l'interfaccia web, e la stessa approvazione collega questo computer per l'agente.",
"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.": "L'app accede al suo server una volta, nel suo browser, come ogni dispositivo. Poi può leggere e scrivere le sue chat, come l'interfaccia web.",
"The connection to the reply was lost: %(why)s": "La connessione alla risposta si è interrotta: %(why)s",
"The most an agent chat may do here": "Il massimo che una chat agente può fare qui", "The most an agent chat may do here": "Il massimo che una chat agente può fare qui",
"The reply is ready.": "La risposta è pronta.",
"Thinking": "Ragionamento",
"This computer": "Questo computer",
"This computer is not linked to the server yet.": "Questo computer non è ancora collegato al server.", "This computer is not linked to the server yet.": "Questo computer non è ancora collegato al server.",
"This file is too big to show here (%(size)s).": "Questo file è troppo grande per essere mostrato (%(size)s).",
"This is not a text file; it cannot be shown here.": "Questo non è un file di testo; non può essere mostrato qui.",
"Thought": "Ha ragionato",
"Try again": "Riprova",
"Waiting for the browser…": "In attesa del browser…",
"Waiting for the reply before it": "In attesa della risposta precedente",
"Waiting for your answer": "In attesa della sua risposta",
"Writing…": "Sta scrivendo…",
"Your answer": "La sua risposta",
"agent off": "agente spento", "agent off": "agente spento",
"agent running": "agente attivo", "agent running": "agente attivo",
"agent running (service)": "agente attivo (servizio)", "agent running (service)": "agente attivo (servizio)",
@@ -329,48 +767,121 @@ const LEMBAS_I18N = {
"update %(version)s": "aggiorna a %(version)s" "update %(version)s": "aggiorna a %(version)s"
}, },
"pl": { "pl": {
"%(n)s tokens": "Tokeny: %(n)s",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Usługa LLeMbas CLI już tu działa (pid %(pid)s); jest agentem tego komputera.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Usługa LLeMbas CLI już tu działa (pid %(pid)s); jest agentem tego komputera.",
"A chat asking for more is lowered to this.": "Czat, który prosi o więcej, zostaje ograniczony do tego.", "A chat asking for more is lowered to this.": "Czat, który prosi o więcej, zostaje ograniczony do tego.",
"A chat asking for more than the folder allows is lowered to it.": "Czat, który chce więcej, niż folder pozwala, zostaje do tego obniżony.",
"A shell as you, in one of these folders.": "Powłoka na Twoim koncie, w jednym z tych folderów.", "A shell as you, in one of these folders.": "Powłoka na Twoim koncie, w jednym z tych folderów.",
"About": "O aplikacji",
"Add folder…": "Dodaj folder…", "Add folder…": "Dodaj folder…",
"Address of your LLeMbas server": "Adres Twojego serwera LLeMbas", "Address of your LLeMbas server": "Adres Twojego serwera LLeMbas",
"Agent": "Agent", "Agent": "Agent",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Czaty agenta z Twojego serwera mogą pracować w tych folderach i we wszystkim, co zawierają — nigdzie indziej na tym komputerze. Uruchom czat na karcie Agenci na serwerze, na urządzeniu „Lokalne”.", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Czaty z agentem mogą pracować w tych folderach i we wszystkim, co zawierają — nigdzie indziej na tym komputerze.",
"Agent stopped: %(why)s": "Agent się zatrzymał: %(why)s", "Agent stopped: %(why)s": "Agent się zatrzymał: %(why)s",
"Allow": "Pozwól",
"Always allow": "Zawsze zezwalaj",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Czat z agentem pracuje w folderze na tym komputerze, przez agenta wbudowanego w tę aplikację.",
"Another device": "Inne urządzenie",
"Answer": "Odpowiedź",
"Approve this code in the page that opened in your browser:": "Zatwierdź ten kod na stronie, która otworzyła się w przeglądarce:",
"Auto — asks nothing": "Auto — o nic nie pyta", "Auto — asks nothing": "Auto — o nic nie pyta",
"Cancel": "Anuluj",
"Carry it out": "Wykonaj",
"Chats": "Czaty",
"Check for updates…": "Sprawdź aktualizacje…",
"Close": "Zamknij", "Close": "Zamknij",
"Close (LLeMbas stays in the tray)": "Zamknij (LLeMbas zostaje w zasobniku systemowym)", "Close (LLeMbas stays in the tray)": "Zamknij (LLeMbas zostaje w zasobniku systemowym)",
"Command": "Polecenie",
"Connect": "Połącz", "Connect": "Połącz",
"Copy": "Kopiuj",
"Cut short here; the whole of it is in the web interface.": "Tu skrócone; całość jest w interfejsie WWW.",
"Deny": "Odrzuć",
"Earlier messages": "Wcześniejsze wiadomości",
"Edit — asks before commands": "Edit — pyta przed poleceniami", "Edit — asks before commands": "Edit — pyta przed poleceniami",
"Empty folder": "Pusty folder",
"Folder": "Folder",
"Install LLeMbas %(version)s…": "Zainstaluj LLeMbas %(version)s…",
"LLeMbas %(version)s is available (this is %(current)s)": "Dostępny jest LLeMbas %(version)s (to jest %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "Dostępny jest LLeMbas %(version)s (to jest %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop wymaga serwera LLeMbas 2.0 lub nowszego.",
"Let the server open a terminal here": "Pozwól serwerowi otworzyć tu terminal", "Let the server open a terminal here": "Pozwól serwerowi otworzyć tu terminal",
"Link again": "Połącz ponownie", "Link again": "Połącz ponownie",
"Link this computer": "Połącz ten komputer", "Link this computer": "Połącz ten komputer",
"Loading…": "Wczytywanie…",
"Looking for a LLeMbas server there…": "Szukam tam serwera LLeMbas…", "Looking for a LLeMbas server there…": "Szukam tam serwera LLeMbas…",
"Manual — asks first": "Manual — najpierw pyta", "Manual — asks first": "Manual — najpierw pyta",
"Maximise": "Maksymalizuj", "Maximise": "Maksymalizuj",
"Menu": "Menu", "Menu": "Menu",
"Minimise": "Minimalizuj", "Minimise": "Minimalizuj",
"Mode": "Tryb",
"Model": "Model",
"More": "Więcej",
"New agent chat": "Nowy czat z agentem",
"New chat": "Nowy czat",
"No agent chat yet. Start one: it works in a folder on this computer.": "Jeszcze nie ma czatu z agentem. Zacznij go: pracuje w folderze na tym komputerze.",
"No chat yet.": "Jeszcze nie ma czatu.",
"No folder yet: add one under Settings, Project folders.": "Jeszcze nie ma folderu: dodaj go w Ustawieniach, Foldery projektów.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Nie ma jeszcze folderu: agent nie może nic otworzyć. Dodaj folder projektu.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Nie ma jeszcze folderu: agent nie może nic otworzyć. Dodaj folder projektu.",
"Open the server": "Otwórz serwer", "No model is available to this account.": "Dla tego konta nie jest dostępny żaden model.",
"No project folder yet: the agent can open nothing on this computer.": "Jeszcze nie ma folderu projektu: agent nie może niczego otworzyć na tym komputerze.",
"Not yet": "Jeszcze nie",
"Notifications": "Powiadomienia",
"Only the agent's folders: Settings, Project folders.": "Tylko foldery agenta: Ustawienia, Foldery projektów.",
"Open in the viewer": "Otwórz w podglądzie",
"Open the page again": "Otwórz stronę ponownie",
"Or type an answer": "Albo wpisz odpowiedź",
"Other devices": "Inne urządzenia",
"Page": "Strona",
"Plan — changes nothing": "Plan — niczego nie zmienia", "Plan — changes nothing": "Plan — niczego nie zmienia",
"Preview": "Podgląd",
"Project folders": "Foldery projektów", "Project folders": "Foldery projektów",
"Read again": "Wczytaj ponownie",
"Recommended": "Zalecane",
"Remove": "Usuń", "Remove": "Usuń",
"Remove %(dir)s": "Usuń %(dir)s", "Remove %(dir)s": "Usuń %(dir)s",
"Restore": "Przywróć", "Restore": "Przywróć",
"Send": "Wyślij",
"Server": "Serwer", "Server": "Serwer",
"Sign in there as you would in a browser.": "Zaloguj się tam tak jak w przeglądarce.", "Settings": "Ustawienia",
"Shown by the agent": "Pokazane przez agenta",
"Sign in": "Zaloguj się",
"Sign out": "Wyloguj się",
"Signed in as %(who)s.": "Zalogowano jako %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Wylogowanie zrzeka się logowania aplikacji na serwerze; połączenie agenta zostaje. Wyloguj się, aby użyć innego serwera.",
"Source": "Źródło",
"Start the agent": "Uruchom agenta", "Start the agent": "Uruchom agenta",
"Stop the agent": "Zatrzymaj agenta", "Stop the agent": "Zatrzymaj agenta",
"Stopped.": "Zatrzymano.",
"Taken in while it ran:": "Przejęte w trakcie działania:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Powiadom mnie, gdy odpowiedź lub praca agenta jest gotowa, a ja jestem gdzie indziej",
"Terminal": "Terminal", "Terminal": "Terminal",
"The agent asks to run %(tool)s:": "Agent prosi o uruchomienie %(tool)s:",
"The agent asks:": "Agent pyta:",
"The agent has finished.": "Agent skończył.",
"The agent is connected, but may open no folder yet: add one below.": "Agent jest połączony, ale nie może jeszcze otworzyć żadnego folderu: dodaj go poniżej.", "The agent is connected, but may open no folder yet: add one below.": "Agent jest połączony, ale nie może jeszcze otworzyć żadnego folderu: dodaj go poniżej.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "Agent nie jest jeszcze połączony z serwerem: uruchom go lub połącz ten komputer w Ustawieniach.",
"The agent is off.": "Agent jest wyłączony.", "The agent is off.": "Agent jest wyłączony.",
"The agent is running: agent chats can use this computer.": "Agent działa: czaty agenta mogą używać tego komputera.", "The agent is running: agent chats can use this computer.": "Agent działa: czaty agenta mogą używać tego komputera.",
"The agent is starting…": "Agent się uruchamia…", "The agent is starting…": "Agent się uruchamia…",
"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.": "Agent pozwala czatom agenta z Twojego serwera pracować w projektach na tym komputerze. To wbudowane LLeMbas CLI, działające w tle, dopóki działa aplikacja.", "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.": "Agent pozwala czatom agenta z Twojego serwera pracować w projektach na tym komputerze. To wbudowane LLeMbas CLI, działające w tle, dopóki działa aplikacja.",
"The agent stopped: %(why)s. Trying again shortly.": "Agent się zatrzymał: %(why)s. Za chwilę spróbuje ponownie.", "The agent stopped: %(why)s. Trying again shortly.": "Agent się zatrzymał: %(why)s. Za chwilę spróbuje ponownie.",
"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.": "Aplikacja loguje się do Twojego serwera raz, w przeglądarce, jak każde urządzenie. Potem może czytać i pisać Twoje czaty, jak interfejs WWW, a to samo zatwierdzenie łączy ten komputer dla agenta.",
"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.": "Aplikacja loguje się do Twojego serwera raz, w przeglądarce, jak każde urządzenie. Potem może czytać i pisać Twoje czaty, jak interfejs WWW.",
"The connection to the reply was lost: %(why)s": "Połączenie z odpowiedzią zostało zerwane: %(why)s",
"The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić", "The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić",
"The reply is ready.": "Odpowiedź jest gotowa.",
"Thinking": "Rozumowanie",
"This computer": "Ten komputer",
"This computer is not linked to the server yet.": "Ten komputer nie jest jeszcze połączony z serwerem.", "This computer is not linked to the server yet.": "Ten komputer nie jest jeszcze połączony z serwerem.",
"This file is too big to show here (%(size)s).": "Ten plik jest za duży, by go tu pokazać (%(size)s).",
"This is not a text file; it cannot be shown here.": "To nie jest plik tekstowy; nie można go tu pokazać.",
"Thought": "Rozumowanie",
"Try again": "Spróbuj ponownie",
"Waiting for the browser…": "Czekam na przeglądarkę…",
"Waiting for the reply before it": "Czeka na odpowiedź przed nią",
"Waiting for your answer": "Czeka na Twoją odpowiedź",
"Writing…": "Pisze…",
"Your answer": "Twoja odpowiedź",
"agent off": "agent wyłączony", "agent off": "agent wyłączony",
"agent running": "agent działa", "agent running": "agent działa",
"agent running (service)": "agent działa (usługa)", "agent running (service)": "agent działa (usługa)",
@@ -382,48 +893,121 @@ const LEMBAS_I18N = {
"update %(version)s": "aktualizacja %(version)s" "update %(version)s": "aktualizacja %(version)s"
}, },
"sk": { "sk": {
"%(n)s tokens": "tokeny: %(n)s",
"%(name)s, LLeMbas %(version)s.": "%(name)s, LLeMbas %(version)s.",
"A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Služba LLeMbas CLI tu už beží (pid %(pid)s); je agentom tohto počítača.", "A LLeMbas CLI service already runs here (pid %(pid)s); it is this computer's agent.": "Služba LLeMbas CLI tu už beží (pid %(pid)s); je agentom tohto počítača.",
"A chat asking for more is lowered to this.": "Konverzácia, ktorá chce viac, je znížená na toto.", "A chat asking for more is lowered to this.": "Konverzácia, ktorá chce viac, je znížená na toto.",
"A chat asking for more than the folder allows is lowered to it.": "Konverzácia, ktorá chce viac, než priečinok dovoľuje, je znížená na to.",
"A shell as you, in one of these folders.": "Shell pod vaším účtom, v jednom z týchto priečinkov.", "A shell as you, in one of these folders.": "Shell pod vaším účtom, v jednom z týchto priečinkov.",
"About": "O aplikácii",
"Add folder…": "Pridať priečinok…", "Add folder…": "Pridať priečinok…",
"Address of your LLeMbas server": "Adresa vášho servera LLeMbas", "Address of your LLeMbas server": "Adresa vášho servera LLeMbas",
"Agent": "Agent", "Agent": "Agent",
"Agent chats from your server can work in these folders and anything inside them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.": "Agentské konverzácie z vášho servera môžu pracovať v týchto priečinkoch a vo všetkom, čo obsahujú — nikde inde na tomto počítači. Spustite ju na serveri na karte Agenti, na zariadení „Lokálny“.", "Agent chats can work in these folders and anything inside them — nowhere else on this computer.": "Agentské konverzácie môžu pracovať v týchto priečinkoch a vo všetkom, čo obsahujú — nikde inde na tomto počítači.",
"Agent stopped: %(why)s": "Agent sa zastavil: %(why)s", "Agent stopped: %(why)s": "Agent sa zastavil: %(why)s",
"Allow": "Povoliť",
"Always allow": "Vždy povoliť",
"An agent chat works in a folder on this computer, through the agent built into this app.": "Agentská konverzácia pracuje v priečinku na tomto počítači cez agenta vstavaného v tejto aplikácii.",
"Another device": "Iné zariadenie",
"Answer": "Odpovedať",
"Approve this code in the page that opened in your browser:": "Potvrďte tento kód na stránke, ktorá sa otvorila vo vašom prehliadači:",
"Auto — asks nothing": "Auto — na nič sa nepýta", "Auto — asks nothing": "Auto — na nič sa nepýta",
"Cancel": "Zrušiť",
"Carry it out": "Vykonať to",
"Chats": "Konverzácie",
"Check for updates…": "Skontrolovať aktualizácie…",
"Close": "Zavrieť", "Close": "Zavrieť",
"Close (LLeMbas stays in the tray)": "Zavrieť (LLeMbas zostane v oznamovacej oblasti)", "Close (LLeMbas stays in the tray)": "Zavrieť (LLeMbas zostane v oznamovacej oblasti)",
"Command": "Príkaz",
"Connect": "Pripojiť", "Connect": "Pripojiť",
"Copy": "Kopírovať",
"Cut short here; the whole of it is in the web interface.": "Tu skrátené; celé je to vo webovom rozhraní.",
"Deny": "Odmietnuť",
"Earlier messages": "Staršie správy",
"Edit — asks before commands": "Edit — pýta sa pred príkazmi", "Edit — asks before commands": "Edit — pýta sa pred príkazmi",
"Empty folder": "Prázdny priečinok",
"Folder": "Priečinok",
"Install LLeMbas %(version)s…": "Nainštalovať LLeMbas %(version)s…",
"LLeMbas %(version)s is available (this is %(current)s)": "Je k dispozícii LLeMbas %(version)s (toto je %(current)s)", "LLeMbas %(version)s is available (this is %(current)s)": "Je k dispozícii LLeMbas %(version)s (toto je %(current)s)",
"LLeMbas Desktop needs a LLeMbas 2.0 server or later.": "LLeMbas Desktop potrebuje server LLeMbas 2.0 alebo novší.",
"Let the server open a terminal here": "Dovoliť serveru otvoriť tu terminál", "Let the server open a terminal here": "Dovoliť serveru otvoriť tu terminál",
"Link again": "Prepojiť znova", "Link again": "Prepojiť znova",
"Link this computer": "Prepojiť tento počítač", "Link this computer": "Prepojiť tento počítač",
"Loading…": "Načítava sa…",
"Looking for a LLeMbas server there…": "Hľadám tam server LLeMbas…", "Looking for a LLeMbas server there…": "Hľadám tam server LLeMbas…",
"Manual — asks first": "Manual — najprv sa opýta", "Manual — asks first": "Manual — najprv sa opýta",
"Maximise": "Maximalizovať", "Maximise": "Maximalizovať",
"Menu": "Ponuka", "Menu": "Ponuka",
"Minimise": "Minimalizovať", "Minimise": "Minimalizovať",
"Mode": "Režim",
"Model": "Model",
"More": "Viac",
"New agent chat": "Nová agentská konverzácia",
"New chat": "Nová konverzácia",
"No agent chat yet. Start one: it works in a folder on this computer.": "Zatiaľ žiadna agentská konverzácia. Začnite ju: pracuje v priečinku na tomto počítači.",
"No chat yet.": "Zatiaľ žiadna konverzácia.",
"No folder yet: add one under Settings, Project folders.": "Zatiaľ žiadny priečinok: pridajte ho v Nastaveniach, Priečinky projektov.",
"No folder yet: the agent can open nothing. Add the folder of a project.": "Zatiaľ žiadny priečinok: agent nemôže nič otvoriť. Pridajte priečinok projektu.", "No folder yet: the agent can open nothing. Add the folder of a project.": "Zatiaľ žiadny priečinok: agent nemôže nič otvoriť. Pridajte priečinok projektu.",
"Open the server": "Otvoriť server", "No model is available to this account.": "Tomuto účtu nie je dostupný žiadny model.",
"No project folder yet: the agent can open nothing on this computer.": "Zatiaľ žiadny priečinok projektu: agent na tomto počítači nemôže nič otvoriť.",
"Not yet": "Ešte nie",
"Notifications": "Upozornenia",
"Only the agent's folders: Settings, Project folders.": "Len priečinky agenta: Nastavenia, Priečinky projektov.",
"Open in the viewer": "Otvoriť v prehliadači súborov",
"Open the page again": "Otvoriť stránku znova",
"Or type an answer": "Alebo napíšte odpoveď",
"Other devices": "Iné zariadenia",
"Page": "Stránka",
"Plan — changes nothing": "Plan — nič nemení", "Plan — changes nothing": "Plan — nič nemení",
"Preview": "Náhľad",
"Project folders": "Priečinky projektov", "Project folders": "Priečinky projektov",
"Read again": "Načítať znova",
"Recommended": "Odporúčané",
"Remove": "Odstrániť", "Remove": "Odstrániť",
"Remove %(dir)s": "Odstrániť %(dir)s", "Remove %(dir)s": "Odstrániť %(dir)s",
"Restore": "Obnoviť", "Restore": "Obnoviť",
"Send": "Poslať",
"Server": "Server", "Server": "Server",
"Sign in there as you would in a browser.": "Prihláste sa tam ako v prehliadači.", "Settings": "Nastavenia",
"Shown by the agent": "Ukázal agent",
"Sign in": "Prihlásiť sa",
"Sign out": "Odhlásiť sa",
"Signed in as %(who)s.": "Prihlásený ako %(who)s.",
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlásením sa prihlásenie aplikácie na serveri zruší; prepojenie agenta zostane. Ak chcete použiť iný server, odhláste sa.",
"Source": "Zdroj",
"Start the agent": "Spustiť agenta", "Start the agent": "Spustiť agenta",
"Stop the agent": "Zastaviť agenta", "Stop the agent": "Zastaviť agenta",
"Stopped.": "Zastavené.",
"Taken in while it ran:": "Prevzaté počas behu:",
"Tell me when a reply or an agent's work is finished while I am elsewhere": "Upozorniť ma, keď je odpoveď alebo práca agenta hotová, kým som inde",
"Terminal": "Terminál", "Terminal": "Terminál",
"The agent asks to run %(tool)s:": "Agent žiada spustiť %(tool)s:",
"The agent asks:": "Agent sa pýta:",
"The agent has finished.": "Agent dokončil prácu.",
"The agent is connected, but may open no folder yet: add one below.": "Agent je pripojený, ale zatiaľ nesmie otvoriť žiadny priečinok: pridajte ho nižšie.", "The agent is connected, but may open no folder yet: add one below.": "Agent je pripojený, ale zatiaľ nesmie otvoriť žiadny priečinok: pridajte ho nižšie.",
"The agent is not connected to the server yet: start it, or link this computer, under Settings.": "Agent ešte nie je pripojený k serveru: spustite ho alebo prepojte tento počítač v Nastaveniach.",
"The agent is off.": "Agent je vypnutý.", "The agent is off.": "Agent je vypnutý.",
"The agent is running: agent chats can use this computer.": "Agent beží: agentské konverzácie môžu používať tento počítač.", "The agent is running: agent chats can use this computer.": "Agent beží: agentské konverzácie môžu používať tento počítač.",
"The agent is starting…": "Agent sa spúšťa…", "The agent is starting…": "Agent sa spúšťa…",
"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.": "Agent umožňuje agentským konverzáciám na vašom serveri pracovať v projektoch na tomto počítači. Je to vstavané LLeMbas CLI, ktoré beží na pozadí, kým beží aplikácia.", "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.": "Agent umožňuje agentským konverzáciám na vašom serveri pracovať v projektoch na tomto počítači. Je to vstavané LLeMbas CLI, ktoré beží na pozadí, kým beží aplikácia.",
"The agent stopped: %(why)s. Trying again shortly.": "Agent sa zastavil: %(why)s. O chvíľu to skúsi znova.", "The agent stopped: %(why)s. Trying again shortly.": "Agent sa zastavil: %(why)s. O chvíľu to skúsi znova.",
"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.": "Aplikácia sa na váš server prihlási raz, vo vašom prehliadači, ako každé zariadenie. Potom môže čítať a písať vaše konverzácie, ako webové rozhranie, a to isté schválenie prepojí tento počítač pre agenta.",
"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.": "Aplikácia sa na váš server prihlási raz, vo vašom prehliadači, ako každé zariadenie. Potom môže čítať a písať vaše konverzácie, ako webové rozhranie.",
"The connection to the reply was lost: %(why)s": "Spojenie s odpoveďou sa prerušilo: %(why)s",
"The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie", "The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie",
"The reply is ready.": "Odpoveď je hotová.",
"Thinking": "Premýšľa",
"This computer": "Tento počítač",
"This computer is not linked to the server yet.": "Tento počítač ešte nie je prepojený so serverom.", "This computer is not linked to the server yet.": "Tento počítač ešte nie je prepojený so serverom.",
"This file is too big to show here (%(size)s).": "Tento súbor je príliš veľký na zobrazenie (%(size)s).",
"This is not a text file; it cannot be shown here.": "Toto nie je textový súbor; nedá sa tu zobraziť.",
"Thought": "Premýšľal",
"Try again": "Skúsiť znova",
"Waiting for the browser…": "Čaká sa na prehliadač…",
"Waiting for the reply before it": "Čaká na odpoveď pred ňou",
"Waiting for your answer": "Čaká na vašu odpoveď",
"Writing…": "Píše sa…",
"Your answer": "Vaša odpoveď",
"agent off": "agent vypnutý", "agent off": "agent vypnutý",
"agent running": "agent beží", "agent running": "agent beží",
"agent running (service)": "agent beží (služba)", "agent running (service)": "agent beží (služba)",
+104 -60
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en" data-theme="moria" data-base="moria">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -7,72 +7,116 @@
<title>LLeMbas</title> <title>LLeMbas</title>
<link rel="stylesheet" href="tokens.css"> <link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
<script src="theme.js"></script> <script src="i18n.js"></script>
<script src="i18n.js" defer></script> <script type="module" src="js/main.js"></script>
<script src="app.js" defer></script>
</head> </head>
<body> <body>
<main class="card" aria-labelledby="title"> <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>
<div class="setup" id="setup" hidden>
<main class="card" aria-labelledby="setup-title">
<header class="brand"> <header class="brand">
<img src="logo-mark.svg" alt="" width="40" height="40"> <img src="logo-mark.svg" alt="" width="40" height="40">
<div> <div>
<h1 id="title"><b>LL</b>e<b>M</b>bas</h1> <h1 id="setup-title"><b>LL</b>e<b>M</b>bas</h1>
<p class="muted">Desktop <span id="version"></span></p> <p class="muted">Desktop <span id="setup-version"></span></p>
</div> </div>
</header> </header>
<div class="setup__body" id="setup-body"></div>
<section aria-labelledby="server-h">
<h2 id="server-h" data-i18n>Server</h2>
<form id="server-form" class="field" novalidate>
<label for="address" data-i18n>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" data-i18n>Connect</button>
</div>
<p class="hint" id="server-hint" aria-live="polite" data-i18n>Sign in there as you would in a browser.</p>
</form>
</section>
<section aria-labelledby="agent-h" id="agent-section" hidden>
<h2 id="agent-h" data-i18n>Agent</h2>
<p class="muted" data-i18n>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" data-i18n>Link this computer</button>
<button type="button" class="btn" id="toggle" data-i18n>Start the agent</button>
<button type="button" class="btn btn--primary" id="open" data-i18n>Open the server</button>
</div>
</section>
<section aria-labelledby="folders-h" id="folders-section" hidden>
<h2 id="folders-h" data-i18n>Project folders</h2>
<p class="muted" data-i18n>Agent chats from your server can work in these folders and anything inside
them — nowhere else on this computer. Start one in the server's Agents tab, on “Local”.</p>
<ul class="folders" id="folders"></ul>
<p class="status" id="folders-status" aria-live="polite"></p>
<div class="row row--buttons">
<button type="button" class="btn btn--primary" id="add-folder" data-i18n>Add folder…</button>
</div>
<div class="settings-grid">
<div class="field">
<label for="max-mode" data-i18n>The most an agent chat may do here</label>
<select id="max-mode">
<option value="manual" data-i18n>Manual — asks first</option>
<option value="edit" data-i18n>Edit — asks before commands</option>
<option value="auto" data-i18n>Auto — asks nothing</option>
<option value="plan" data-i18n>Plan — changes nothing</option>
</select>
<p class="hint" data-i18n>A chat asking for more is lowered to this.</p>
</div>
<div class="field">
<span class="label" data-i18n>Terminal</span>
<label class="check"><input type="checkbox" id="terminal"> <span data-i18n>Let the server open a terminal here</span></label>
<p class="hint" data-i18n>A shell as you, in one of these folders.</p>
</div>
</div>
</section>
</main> </main>
</div>
<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> </body>
</html> </html>
+72
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]}`
}
+170
View File
@@ -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)
}
+81
View File
@@ -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
View File
@@ -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()
+347
View File
@@ -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
}
+178
View File
@@ -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
View File
@@ -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())
})
}
+20 -24
View File
@@ -1,14 +1,9 @@
// The title bar. Moving and double-click maximising come from `data-tauri-drag-region`; the // The title bar: the app's own, in the page (the window has no system decoration). Moving and
// buttons call the window directly; the menu and the agent pill open the app's native menu (the // double-click maximising come from `data-tauri-drag-region`; the edges resize the window (Tauri
// tray's items), which can reach past this 36-pixel-high page. The bar is painted in the theme // does that for an undecorated one); the menu and the agent pill open the app's native menu.
// the app reports in `status` (the server page's); every word goes through `t` (i18n.js).
"use strict"
const { invoke } = window.__TAURI__.core import { $, icon, ICONS } from "./dom.js"
const { listen } = window.__TAURI__.event import { invoke, listen, win } from "./bridge.js"
const win = window.__TAURI__.window.getCurrentWindow()
const $ = (id) => document.getElementById(id)
const WORDS = { const WORDS = {
off: () => t("agent off"), off: () => t("agent off"),
@@ -20,8 +15,7 @@ const WORDS = {
no_folders: () => t("agent: no folders"), no_folders: () => t("agent: no folders"),
} }
function render(s) { export function renderBar(s) {
window.lembasTheme.apply(s.theme)
let host = "" let host = ""
try { try {
host = s.server ? new URL(s.server).host : "" host = s.server ? new URL(s.server).host : ""
@@ -36,31 +30,33 @@ function render(s) {
$("agent").title = state === "failed" ? t("Agent stopped: %(why)s", { why: s.agent.detail }) : t("Agent") $("agent").title = state === "failed" ? t("Agent stopped: %(why)s", { why: s.agent.detail }) : t("Agent")
$("update").hidden = !s.update $("update").hidden = !s.update
$("update").textContent = s.update ? t("update %(version)s", { version: s.update }) : t("update") $("update").textContent = s.update ? t("update %(version)s", { version: s.update }) : t("update")
$("update").title = s.update $("update").title = s.update ? t("LLeMbas %(version)s is available (this is %(current)s)", { version: s.update, current: s.version }) : ""
? t("LLeMbas %(version)s is available (this is %(current)s)", { version: s.update, current: s.version })
: ""
} }
function maximized(on) { function maximized(on) {
const b = $("maximize") const b = $("maximize")
b.toggleAttribute("data-maximized", on) b.replaceChildren(icon(on ? ICONS.restore : ICONS.max))
b.setAttribute("aria-label", on ? t("Restore") : t("Maximise")) b.setAttribute("aria-label", on ? t("Restore") : t("Maximise"))
b.title = on ? t("Restore") : t("Maximise") b.title = on ? t("Restore") : t("Maximise")
} }
$("minimize").addEventListener("click", () => win.minimize())
$("maximize").addEventListener("click", () => win.toggleMaximize())
$("close").addEventListener("click", () => win.close())
$("update").addEventListener("click", () => invoke("update_app"))
// The menu opens under the button that was clicked: its bottom-left corner, in page pixels. // The menu opens under the button that was clicked: its bottom-left corner, in page pixels.
function menuAt(button) { function menuAt(button) {
const r = button.getBoundingClientRect() const r = button.getBoundingClientRect()
return invoke("titlebar_menu", { x: r.left, y: r.bottom }) 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)) $("menu").addEventListener("click", (e) => menuAt(e.currentTarget))
$("agent").addEventListener("click", (e) => menuAt(e.currentTarget)) $("agent").addEventListener("click", (e) => menuAt(e.currentTarget))
listen("status", (e) => render(e.payload))
listen("maximized", (e) => maximized(!!e.payload)) listen("maximized", (e) => maximized(!!e.payload))
invoke("get_status").then(render) win.isMaximized().then(maximized).catch(() => maximized(false))
win.isMaximized().then(maximized) }
+124
View File
@@ -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()
})
}
-15
View File
@@ -1,15 +0,0 @@
// The app's own pages are painted in the server page's theme, which the app reports in `status`
// (see src-tauri/src/lib.rs: the content view says, the app remembers). tokens.css selects a
// theme by `data-theme` and `data-base` on <html>, as the server's pages do; with nothing set
// it is Moria, the server's own default, so the page needs no script to be right at first.
"use strict"
window.lembasTheme = {
apply(name) {
if (name !== "moria" && name !== "shire") return
const root = document.documentElement
if (root.dataset.theme === name) return
root.dataset.theme = name
root.dataset.base = name
},
}
-99
View File
@@ -1,99 +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. Colours, type and space are
tokens from tokens.css (the server's); the bar's height is this page's own. */
* { box-sizing: border-box; }
:root {
/* `TITLE_H` in src-tauri/src/lib.rs is this number: the app lays the bar out at it. */
--titlebar-h: 36px;
}
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) · update · agent · menu · sep · 3 controls */
grid-template-columns: 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);
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: var(--outline-w) solid var(--accent); outline-offset: calc(-1 * var(--outline-w)); }
.bar__agent {
display: grid;
grid-template-columns: var(--sp-2) auto;
gap: var(--sp-2);
align-items: center;
padding: 0 var(--sp-3);
font-weight: 400;
}
.dot { width: var(--sp-2); height: var(--sp-2); 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,
.bar__agent[data-state="no_folders"] .dot { background: var(--warning); }
.bar__update {
padding: 0 var(--sp-3);
color: var(--accent-ink);
background: var(--accent);
}
.bar__update:hover { color: var(--accent-ink); background: var(--accent-hover); }
/* Hidden keeps its grid track (auto, so zero wide): the slots after it never move. */
.bar__update[hidden] { display: block; width: 0; padding: 0; overflow: hidden; }
.bar__sep { width: var(--border-w); height: var(--sp-5); background: var(--border); }
.bar__btn { display: grid; place-items: center; }
.bar__btn svg { width: 16px; height: 16px; }
.bar__btn--close:hover { color: var(--ink-inverse); 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; }
}
-39
View File
@@ -1,39 +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="theme.js"></script>
<script src="i18n.js" defer></script>
<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__update" id="update" type="button" hidden data-i18n>update</button>
<button class="bar__agent" id="agent" type="button" title="Agent" data-i18n-title>
<span class="dot" aria-hidden="true"></span><span id="agent-text" data-i18n>agent off</span>
</button>
<button class="bar__btn" id="menu" type="button" aria-label="Menu" title="Menu" data-i18n-aria-label data-i18n-title>
<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" data-i18n-aria-label data-i18n-title>
<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" data-i18n-aria-label data-i18n-title>
<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)" data-i18n-aria-label data-i18n-title>
<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>
+6 -32
View File
@@ -1,4 +1,4 @@
/* LLeMbas v1.3.0 (61cf4c267c98) static/css/tokens.css, copied by scripts/fetch-tokens.sh: do not edit. /* LLeMbas v2.0.0-beta.4 (7969067f7260) 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. */ The font-face rules are left out; the app bundles no font. */
/* /*
Design tokens. Design tokens.
@@ -99,37 +99,6 @@
re-wraps everything a program prints. */ re-wraps everything a program prints. */
--terminal-width: 34rem; --terminal-width: 34rem;
--terminal-width-min: 24rem; --terminal-width-min: 24rem;
/* Wider again: a source line is longer than eighty columns once nothing is
re-wrapping it, and this one holds prose as well. The minimum is 24rem =
384px and must equal both `data-resize-min` in chat/_canvas.html and the
lower bound in api/preferences.py:LAYOUT_BOUNDS -- a width outside those
bounds is silently dropped, so the three are pinned equal by a test. */
--canvas-width: 40rem;
--canvas-width-min: 24rem;
/* The prototype panel. The canvas's width, not wider: the page inside is not
laid out at the panel's width anyway (see --proto-desktop-width), so a wider
panel only buys a larger picture at the cost of the conversation beside it
-- at 52rem a 1280px window had 180px of chat left. The minimum is pinned to
`data-resize-min` and LAYOUT_BOUNDS exactly as the canvas's is. */
--prototype-width: 40rem;
--prototype-width-min: 24rem;
/* The two views are two viewports, each scaled to fit the panel rather than
squeezed into it. A desktop design shown at 640px is its tablet layout, and
the switch says "desktop". So the page is laid out at a desktop's width and
the picture is shrunk, the way a browser zoomed out would show it -- and a
phone is an iPhone-sized viewport, the width most designs are checked at.
A frame either way, not an emulation: the page's own media queries answer
to these numbers, which is the whole point of the switch. */
--proto-desktop-width: 80rem;
--proto-phone-width: 24.375rem;
--proto-phone-height: 52.75rem;
/* An inline prototype is as tall as the page it holds, within these. The
floor keeps a frame that has not reported yet from collapsing to nothing;
the ceiling is what the model is told ("under about 600 pixels"), with a
little room, so a page sized in `100vh` cannot grow a frame forever. */
--proto-inline-min: 6rem;
--proto-inline-start: 16rem;
--proto-inline-max: 40rem;
--thread-max-width: 48rem; --thread-max-width: 48rem;
--header-height: 3.5rem; --header-height: 3.5rem;
@@ -352,6 +321,10 @@
--accent-hover: #B4E6F1; --accent-hover: #B4E6F1;
--accent-ink: #061418; --accent-ink: #061418;
--accent-soft: rgba(143, 216, 232, 0.12); --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 /* Mallorn: the brand, the assistant's mark, the leaf badges -- phosphor
green on the dark, the colour of an old terminal's text. */ green on the dark, the colour of an old terminal's text. */
@@ -495,6 +468,7 @@
--accent-hover: #1F4D30; --accent-hover: #1F4D30;
--accent-ink: #F8F3E5; --accent-ink: #F8F3E5;
--accent-soft: rgba(40, 95, 60, 0.1); --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. */ /* The same mallorn as moss, dark enough to be text on parchment. */
--leaf: #3D6B1C; --leaf: #3D6B1C;
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+15
View File
@@ -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
+3
View File
@@ -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
+8
View File
@@ -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
+5
View File
@@ -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
+7
View File
@@ -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
+3
View File
@@ -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
+4
View File
@@ -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
+7
View File
@@ -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
+9
View File
@@ -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
+3
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
/*
The `process` that esm.sh's builds import as "/node/process.mjs" (its Node
polyfill). @lezer/lr reads `process.env.LOG` to decide whether to log its
parse; nothing else here asks. Without this the absolute path is a 404 on
this server, @lezer/lr fails to load, and with it every editor language
that parses: the editor then opens with no highlighting at all.
Mapped by the import map in files/edit.html.
*/
export default { env: {} };
+24
View File
@@ -0,0 +1,24 @@
{
"tag": "v2.0.0-beta.4",
"commit": "7969067f72605db0df7dd877b58d847156373f18",
"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": "f41bc21dc3fd874132e1d9862aae57946ae2604d91330303924c03aecfd3bc21"},
"node-process.mjs": {"server": "0c7460cbd81d8427a882867726b6db43beff3399f045ad942f4a65fef4705981", "copy": "0c7460cbd81d8427a882867726b6db43beff3399f045ad942f4a65fef4705981"}
}
}