Feature set (arch pkgbuild, intent, migration) - #1
Merged
gabrielefronze merged 27 commits intoSep 12, 2026
Merged
gabrielefronze merged 27 commits into
gabrielefronze merged 27 commits into
Conversation
Renders packaging/archlinux/PKGBUILD.in against the current checkout and builds it in a sandboxed extra-x86_64-build chroot via scripts/build-arch.sh, with a persistent vcpkg binary cache bind-mounted into the chroot so Qt6/ gRPC/Boost/QEMU only compile from source once.
repo_source (file://...#branch=...) contains a literal '#', which broke the '#'-delimited sed s### substitution. Use bash parameter substitution instead, which has no delimiter to collide with.
This checkout has no git tags, so `git describe --tags --long` fails: - pkgver()'s previous `... | sed ... || echo 0.0.0` fallback never triggered (the pipeline's exit status is sed's, not git describe's), so pkgver() returned an empty string and makepkg aborted. - The project's own CMake versioning (src/cmake/versioning.cmake) hits the same wall and errors out during configure. Fix pkgver()'s fallback properly, and have prepare() tag the disposable build clone locally (never the original repo) so both pkgver() and versioning.cmake's own `git describe` succeed unmodified.
…MU build The vcpkg qemu port's Linux path runs QEMU's real upstream ./configure (3rd-party/vcpkg-ports/qemu/portfile.cmake), which for QEMU >=8 shells out to meson to generate the actual build — meson was missing from makedepends, which is why ./configure failed. --enable-virtfs also needs libcap-ng/attr, and the vendored dtc subproject (patch 0005) needs flex alongside bison.
configure's actual failure (found via config-x64-linux-release-rel-err.log): "found no usable distlib, please install it" from QEMU's mkvenv step. Arch's python package ships the PEP 668 EXTERNALLY-MANAGED marker, so mkvenv's own pip-install fallback for distlib is blocked; installing python-distlib as a system package satisfies the check without pip.
versioning.cmake calls plain \`git describe\` (no --tags), which only considers annotated tags — the lightweight tag prepare() created wasn't enough, even though pkgver()'s own \`git describe --tags\` accepted it. Create an annotated tag instead, with an inline committer identity since a fresh chroot's git has no global user.name/user.email configured.
CMakeLists.txt's own version check, \`if (NOT CMAKE_MATCH_1)\`, treats the *string* "0" as false in CMake's if(), so a parsed major version of "0" makes it wrongly report "Failed to parse MAJOR.MINOR.PATCH" even though the regex matched correctly. Avoid tripping that latent bug by tagging with a non-zero major instead of touching the main CMakeLists.txt.
CMake's rxx/CMakeLists.txt find_program()s cargo directly; BUILD.linux.md covers installing it via rustup for a manual dev build, but the PKGBUILD never declared it. Arch's rust package provides both rustc and cargo.
… step The GUI's hotkey_manager Flutter plugin requires keybinder-3.0 (libkeybinder3), which is AUR-only. A sandboxed extra-x86_64-build chroot can't reach the AUR, so document building it once locally and injecting it into the chroot via makechrootpkg's -I, in both the PKGBUILD comments and BUILD.linux.md. Also declare it in `depends` since the GUI links against it at runtime, not just at build time.
Building on Arch's newer Clang/GTK surfaces two -Werror failures that this project's usual (older/Ubuntu) toolchain never hits: - my_application.cc calls gdk_pixbuf_new_from_xpm_data, deprecated in GTK >=2.44. - The vendored hotkey_manager_linux Flutter plugin (fetched from pub.dev, not part of this repo) has a genuine but harmless uninitialized-variable-on-one-branch bug, caught by Clang's -Wsometimes-uninitialized (GCC: -Wmaybe-uninitialized). apply_standard_settings() in src/client/gui/linux/CMakeLists.txt is shared by the main GUI binary and every plugin target (per its own comment), so downgrading just these two warning classes to non-fatal here fixes both without touching the third-party plugin's source or weakening -Wall/-Werror generally.
Newer GCC (GCC 16, as shipped by CachyOS/Arch) mis-analyzes object layout while inlining httplib::SSLClient's destructor across vcpkg's vendored cpp-httplib header (httplib.h) in openai.cpp's proxy_to_backend, reporting bogus out-of-bounds array subscripts on an object it itself just correctly sized via operator new. Scope -Wno-error=array-bounds to the elp_api target only (GCC only), keeping -Warray-bounds as an error everywhere else in the codebase where it's still a useful check.
package() failed with "cannot stat '/build/elp/src/../elpd.service'": a sandboxed extra-x86_64-build chroot only stages the PKGBUILD plus its own source=() entries into the container, not arbitrary files that merely happen to sit next to it on the host (which is how scripts/build-arch.sh places elpd.service). Declare it as a second source=() entry so devtools' staging step copies it in, and reference it via the srcdir-relative path makepkg actually puts it at. The rest of the build succeeded end to end on this run: elpd, elp, the GUI (including the AUR libkeybinder3 dependency), and elp-api all compiled and linked; only packaging the systemd unit failed.
… bloat
The build now succeeds end to end and pacman -U installs it, but namcap
(run automatically by extra-x86_64-build) found real defects that would
break the package on any machine other than the one it was built on:
- license=('GPL3') isn't a valid SPDX identifier; use GPL-3.0-only
(matches this repo's file headers: "version 3" with no "or later").
- libsecret (flutter_secure_storage_linux) and apparmor
(libapparmor.so.1,
used by elpd/libdart_ffi.so) were linked at runtime but missing from
depends (apparmor was only in makedepends).
- Several Flutter plugin .so files and the bundled libdart_ffi.so had
absolute build-tree RPATHs baked in (e.g.
/build/elp/src/build/vcpkg_installed/...): flutter build linux is a
custom command, not a normal install(TARGETS), so CMake's own
install-time RPATH rewrite never touches these files. patchelf them to
$ORIGIN in package() instead (patchelf added to makedepends).
- sshfs_server (src/sshfs_mount/CMakeLists.txt) set INSTALL_RPATH to the
macOS-only @executable_path token unconditionally, which is a silent
no-op on Linux; use $ORIGIN there instead, keeping @executable_path
only under APPLE.
- The debug package was ~4.7GB because vcpkg's entire buildtree (Qt6,
gRPC, QEMU, ...) was being swept into debug symbols; !debug in options
skips generating it.
Drop the libkeybinder3 AUR workaround: it's in Arch's official extra repo
Verified via 'pacman -Si libkeybinder3' (present in [extra], packaged by
an Arch dev, not AUR-only as previously assumed). makepkg now resolves
and installs it automatically as a normal depends entry — no more manual
build-and-inject step needed before running scripts/build-arch.sh.
Drop redundant apparmor makedepends entry (namcap: already in depends)
Being in depends already pulls it in at build time too; listing it in
both arrays just tripped namcap's redundant-makedepend check.
Implements feature 2 of the anvil plan: `elp intent create <name>
--service redis --service postgres` launches a named group of instances
together, tagging each with its intent/role; `elp intent
list|info|delete`
manage the group.
- Proto: 4 new RPCs (intent_create/list/info/delete) plus `intent`/
`intent_role` fields on LaunchRequest/DetailedInfoItem so membership
is
visible via the existing launch/info/list surface too.
- IntentSpec (include/multipass/intent_spec.h,
src/utils/intent_spec.cpp):
a lightweight registry (name + ordered members + creation time),
persisted to multipassd-intents.json next to the existing
multipassd-vm-instances.json, following the same tag_invoke/
MP_FILEOPS.write_transactionally pattern as VMSpecs.
- Daemon::intent_create reuses the existing, well-tested
Daemon::create_vm
for each member rather than duplicating VM provisioning. create_vm is
asynchronous (QFutureWatcher-based), so naively looping and blocking
on
each member from the daemon's own thread would deadlock; instead each
member's completion (via a one-shot, self-deleting DaemonRpcContext)
chains the next member's launch, and the last one completes the RPC.
Progress is forwarded into the client-visible IntentCreateReply stream
via a small ServerReaderWriterInterface adapter.
- Two starter service templates (redis, postgres) are embedded in
src/daemon/intent_service_templates.cpp
(data/cloud-init-yaml/cloud-init-
{redis,postgres}.yaml are kept as the human-readable reference
copies);
`elp intent create` also accepts fully custom --instance members.
- CLI: src/client/cli/cmd/intent.{h,cpp}, one command with create/list/
info/delete actions, registered in client.cpp.
Not yet built/tested — proto codegen and Qt signal/slot wiring need a
real
compile to catch what a read-only review can't. GUI integration (launch
form intent picker, list filter) is deferred to a later pass per the
plan.
The intent_create handler set intent/intent_role on each member's internal LaunchRequest, and DetailedInfoItem gained proto fields for them, but create_vm's completion handler only ever captured request->service_id() into VMSpecs.metadata — intent/intent_role were silently dropped, and populate_instance_info never read them back out either. Wire both sides: capture intent/intent_role into metadata alongside service_id, and surface them on `elp info` via a small metadata_string() helper next to the existing service_id_from_specs().
intent_create previously rejected an already-existing name outright, with no way to grow a running intent (e.g. add a cache to an app you already stood up). Adds a new intent_add_member RPC and `elp intent add <name> --service <role>` (same --instance inline mode as create). Factors the async per-member launch chaining out of intent_create into a shared Daemon::launch_intent_members(), so intent_add_member reuses it rather than duplicating the tricky (and only recently validated) chaining logic. Also guards against a narrow race: since the chain is asynchronous, a concurrent intent_delete could remove the intent while members are still being launched; intent_add_member's completion handler checks the registry still has the entry instead of reviving it via intents[name].
…icker Replaces the earlier metadata-tagging-only approach (free-text intent/role fields in the launch form) with a registry-consistent workflow, per feedback: intents are only usable in the GUI once they already exist. - New Intents page (src/client/gui/lib/intents/intents_screen.dart, sidebar entry "Intents"): create an intent with one or more members (role + optional image, template-backed when image is blank), add a member to an existing intent, delete an intent, and see each member's live status — all backed by the daemon's intent_create/add/list/info/delete RPCs via new GrpcClient wrappers and a polling intentsStreamProvider. - Launch form: the intent section is now a dropdown of existing intents only (default "None"), not a free-text field — no more tagging an intent that doesn't exist in the registry. Selecting one launches via intent_add_member instead of a plain launch, so the new instance is properly tracked; mounts and bridged networking are hidden in that mode since intent_add_member doesn't support them yet. - Instance list (vm_table/vms.dart): kept the intent filter dropdown from the previous pass, and added a "Group by intent" toggle that renders one Table per intent (plus one for untagged instances) instead of a single flat table. Not yet analyzed or built — no Dart toolchain available on this host to even run `dart analyze` (the vendored 3rd-party/flutter submodule isn't checked out outside the sandboxed build chroot).
Its constructor was hardcoded to grpc::ServerReaderWriterInterface<IntentCreateReply, IntentCreateRequest>*, so intent_add_member (which needs to wrap an IntentAddMemberReply/Request stream instead) failed to compile with "no matching function for call to IntentMemberLaunchSink::IntentMemberLaunchSink". Templated the class on OuterReply/OuterRequest so both intent_create and intent_add_member can share it, and pass explicit template arguments at each call site.
- intent_create (daemon + CLI) no longer rejects zero members: an intent can be created as a named, empty group and populated later via `elp intent add` / intent_add_member. launch_intent_members already handled an empty launch_requests vector correctly (the base case fires immediately), so no chaining logic changed. - GUI Intents page's create dialog now starts with zero member rows and lets every row be removed, instead of requiring at least one. - Launch form: the intent dropdown gained a "+ Create new intent..." option (calling intent_create with one member) alongside existing intents (intent_add_member), not just existing ones. - IntentMemberRequest gained service_id, copied into the internal LaunchRequest by build_intent_member_launch_requests: a marketplace service deployed into an intent is still recognized as a service instance (isServiceVmInfo/service bindings), not just a plain instance.
Mirrors LaunchForm's intent dropdown ("None" / an existing intent /
"+ Create new intent...") in _ServiceDeployDialog, per the requirement
that
Deploy needs the same treatment as Launch: not only picking an existing
intent, but creating a new one inline too.
_deployIntoIntent parallels LaunchForm's _launchIntoIntent: it builds an
IntentMemberRequest carrying service_id: widget.service.id so the
deployed
instance is still recognized as a service even when it's a member of an
intent rather than launched standalone, and calls intent_create or
intent_add_member instead of the plain launch RPC. The service-instance
binding is still set unconditionally so the GUI treats it as the
deployed
service either way.
Extends the intent feature to LLM model sessions end to end, matching
the
existing VM-instance path rather than adding separate, weaker tagging:
- IntentSpec::Member gains a "kind" field ("vm"/"llm", defaulting to
"vm"
for old persisted registries) so one intent can mix VM and LLM
members.
- IntentMemberRequest gains model_id (+
quant/ctx_size/runtime/max_tokens):
when set, the daemon loads an LLM session via LlmDispatcher/LlmService
instead of launching a VM. build_intent_member_launch_requests and the
new build_intent_member_load_requests partition a mixed member list by
whether model_id is set.
- Daemon::launch_intent_llm_members mirrors launch_intent_members' async
chaining (one member at a time, since load_model is itself async),
reusing
the same self-deleting IntentMemberContext. Unlike VM launches (whose
create_vm completion is already marshaled back to Daemon's thread via
QFutureWatcher), load_model's completion can fire from an
LlmDispatcher
worker-pool thread, so each member's completion is explicitly
re-queued
onto Daemon's own thread before touching `intents`. intent_create/
intent_add_member now run the VM chain then the LLM chain and merge
both
member lists into one IntentSpec.
- LoadedSession gains intent/intent_role fields, persisted in
llm-sessions.json and surfaced in LoadedModelInfo/list_models; reusing
an
already-loaded session for the same model_id now backfills its intent
tag too instead of leaving it untracked.
- intent_list/intent_info/intent_delete branch on member.kind: status
for
an "llm" member comes from LlmService::has_instance (new,
mutex-protected)
via a new LlmDispatcher passthrough, and intent_delete unloads LLM
members
through unload_instances_blocking instead of delete_vm.
- CLI: `elp intent create/add <name> --model role:model_id` adds an LLM
member with default quant/ctx/runtime; `elp llm load --intent X
--intent-role Y` also works for full control over those settings,
tagging
the session but going through the same intent_create/add_member path.
- GUI: the model-load dialog (llm_load.dart) gained the same intent
picker
(existing intent, or create new) already on the Launch form and Deploy
dialog; the Intents page can add an LLM member via a "Model ID" field;
the LLM instances table gained an Intent column and search now matches
on it too.
Fix silent intent launches, add Intents refresh, fix grouped select-all
- Launching/deploying/loading into an intent (LaunchForm, service deploy
dialog, LLM load dialog) now shows a visible "adding.../added/failed"
notification via NotificationsNotifier.addOperation, instead of only a
recent-activity entry that's easy to miss — matching the feedback a
plain
launch already gets via LaunchingNotification.
- Fixed a related navigation bug: after an intent-based launch/deploy,
the
code navigated to elpVm(launchRequest.instanceName)/
serviceInstanceSidebarKey(_request.instanceName) — but the daemon
names
an intent member "<intent>-<role>" itself, ignoring that client-chosen
name, so this went to a sidebar key for an instance that was never
created. Now navigates to the Intents page instead for either path.
- Added a refresh button to the Intents page (intentsStreamProvider
already
polls, but there was no way to force an immediate refresh).
- Fixed "select all" in the instances page's "group by intent" view
selecting every instance across every intent instead of just the group
it was clicked in: each per-group Table reused the same shared
checkbox
TableHeader, whose default childBuilder (SelectAllCheckbox) selects
from
the full, ungrouped instance list. Each group's checkbox column now
gets
its own _GroupSelectAllCheckbox scoped to that group's own instance
ids
(via a new SelectedVmsNotifier.toggleAll).
The Arch package only ever shipped elpd.service, so elp-api (the REST sidecar the GUI depends on for things like the catalogue's per-service gateway CA fetch at https://127.0.0.1:7777/ca.crt) never started unless run manually — every "Deploy" from the Catalogue failed with a connection-refused error against that port. Add elp-api.service (--insecure-no-auth, matching this project's own macOS LaunchDaemon default) alongside elpd.service: new packaging/archlinux/elp-api.service, wired into PKGBUILD.in's source=()/package() and build-arch.sh's staging copy the same way elpd.service already is. BUILD.linux.md now tells installers to `systemctl enable --now elpd elp-api` (Arch packages don't auto-enable services on install).
Implements the migration feature end to end: `elp migrate <name> --to
user@host [--copy]` moves a VM instance or a whole intent (VM and/or LLM
members) to another elp host.
Architecture note (deviates from the original plan): rather than a new
daemon-to-daemon "migrate_import" RPC, the source daemon drives the
target
purely through the target's own local `elp` CLI over SSH. A real
daemon-to-daemon gRPC call would need the target's local.passphrase set
and
a prior `elp authenticate` run (elpd only auto-trusts a client cert over
its unix socket, not over TCP) — going over SSH instead reuses the SSH
access the user already needs to name a host as a target at all, with no
new elp-specific trust setup.
Daemon (src/daemon/daemon.cpp,
migrate/list_network_hosts/add_known_host/
remove_known_host):
- VMSpecs gained image/cloud_init_user_data/remote_name (nothing
persisted
this before — it was discarded right after launch for every instance),
needed to redefine an instance's original spec on a migration target.
- Daemon::migrate stops VM members synchronously (same call `elp stop
--force` uses), snapshots each member's spec (VMSpecs for VM members,
LlmService::instance_info — new — for LLM members), then runs the
actual
ssh/rsync work on a background thread (QtConcurrent, mirroring
create_vm's
own async pattern) so a slow transfer can't block the daemon; only the
commit step (delete source / persist / update intents) is marshaled
back
to the daemon's own thread via QFutureWatcher::finished, as usual.
- A standalone VM is redefined via `elp launch <image> --cloud-init -`,
piping cloud-init through the ssh tunnel's stdin (no remote temp file
needed). An intent's members (VM and/or LLM together) are redefined in
one combined `elp intent create --instance ... --model ...` call,
reusing
the intent feature's own existing member-parsing rather than
duplicating
it — this is also what actually re-registers each member into the
target's own intents map (`elp llm load --intent` alone only tags a
session, it doesn't touch the registry). Mounts are rsynced to the
same
absolute host path on the target, then re-attached with `elp mount`.
- Mode B ("move volume" fast path) is deliberately deferred; v1 only
implements Mode A (redefine + sync data), which is also the only mode
that applies to LLM members at all.
mDNS host discovery (include/multipass/mdns_service.h, new
src/platform/mdns_service*.cpp): elpd advertises itself and browses for
peers on "_elp._tcp", behind one MdnsService interface —
AvahiThreadedPoll
on Linux (new avahi dependency, packaging/archlinux updated), the
classic
dns_sd.h API on macOS (untested — no macOS access in this session), a
permanently-inert no-op elsewhere. A manually-added "known hosts" list
(new list_network_hosts/add_known_host/remove_known_host RPCs, persisted
like intents) is the always-available fallback for hosts mDNS can't
reach
(different subnet/VLAN, or avahi-daemon not running).
GUI: new Migration Hosts page (known-host CRUD) and a migrate dialog
(pick a discovered/known host or type a new one, optional --copy) wired
into the instance details page and each intent's card.
CLI: new `elp migrate` command.
None of this has been build-tested — the mDNS/Avahi and SSH/rsync-over-
child-process pieces especially are first-draft, unverifiable-by-me code
(no second host or avahi-daemon reachable in this environment) and
should
be expected to need real debugging once actually run, the same way the
Arch PKGBUILD did.
Fix avahi pkg-config check: no separate avahi-common.pc on Arch
pkg_check_modules(AVAHI REQUIRED IMPORTED_TARGET avahi-client
avahi-common)
failed during the sandboxed build: Arch's avahi package ships
avahi-client.pc but no avahi-common.pc — avahi-client.pc's own Libs:
line
already includes -lavahi-common, so avahi-common was never a separate
pkg-config module to require in the first place.
Linking elpd failed with undefined vtable/metaObject/qt_metacall/signal symbols for multipass::MdnsService: it's a Q_OBJECT class (like multipass::Process, also declared under include/multipass/), but unlike src/daemon and src/llm (which both set CMAKE_AUTOMOC ON), src/platform never enabled AUTOMOC, so moc never ran on mdns_service.h and none of its signal/vtable implementations were ever generated.
The previous fix (set(CMAKE_AUTOMOC ON) before add_library) should be
sufficient on its own — it's the same pattern src/daemon and src/llm
already use successfully — but the exact same
undefined-vtable/metaObject
link errors for MdnsService recurred after it, unmodified. Setting
AUTOMOC
directly via set_target_properties right after the target is created
removes any possible ambiguity from directory/function variable scoping,
in case that's what's actually going on here.
Actually fix MdnsService moc generation: list the header as a source
Both previous attempts (CMAKE_AUTOMOC variable, then the AUTOMOC target
property) left the exact same undefined vtable/metaObject/signal symbols
—
AUTOMOC was never the problem, header discovery was. Found the real
precedent: src/process/CMakeLists.txt already has a Q_OBJECT header
outside
its own directory (include/multipass/process/process.h) and explicitly
lists it as a library source, rather than relying on it being #included
by
a tracked .cpp. AUTOMOC does not reliably moc a header that way — it
needs
to be an explicit source. Added
${CMAKE_SOURCE_DIR}/include/multipass/mdns_service.h to each platform
variant's add_library() sources, mirroring that pattern exactly.
Real-world testing hit "Permission denied (publickey,password)": elpd runs as root (systemd unit has no User=), so ssh/rsync child processes it spawns use root's own ~/.ssh, not whichever user actually ran `elp migrate` — root's identity is very unlikely to already be authorized on a migration target. Add an optional identity_file (MigrateRequest field 5, `elp migrate --identity <path>`, threaded through as `ssh -i`/`rsync -e "ssh -i ..."`) so a normal user's own key can be used instead of needing a separate one set up for root. Also added the same field to the GUI's migrate dialog.
Migration Hosts' "Add host" dialog now also takes an optional SSH
identity
file, saved per host (KnownHost{target, identity_file}, persisted the
same
way) so it doesn't need retyping on every migration to that host. The
migrate dialog pre-fills its own identity field from the selected host's
saved default when you pick one from the dropdown (still editable, to
override for just that migration) and defaults to empty — i.e. ssh's own
normal key discovery — if neither is set.
Both identity fields are backed by file_selector's openFile() (already a
GUI dependency, used elsewhere for cloud-init/wallpaper imports) via a
"Browse..." button rather than a fixed-extension picker, since private
keys are commonly extensionless (id_ed25519, etc.) — the native file
dialog is the closest desktop-appropriate equivalent to path
autocomplete.
Fix intent picker fields overflowing off-screen in Launch/Deploy dialogs
Both dialogs laid the intent dropdown + "new intent name" + "role in
intent" fields out in a Row, each a fixed 360px wide — up to 1128px
total
(dropdown + both text fields, when creating a new intent), well past
either dialog's actual width. Row doesn't wrap, so the trailing field(s)
were pushed off screen instead of clipped-but-visible, making "role in
intent" effectively invisible whenever a new intent was being created
(and,
to a lesser but still real extent, when picking an existing one too).
Switched to Wrap so fields flow onto their own line instead of
overflowing.
LlmLoad's own intent picker already stacked fields vertically and didn't
have this problem; the Intents page's own member-row fields use Expanded
rather than fixed widths, so they shrink to fit instead of overflowing.
Bump migration ssh/rsync timeouts and add keepalives
Migrating an intent with a slow cloud-init (e.g. a minio instance doing
a
docker pull) was hitting "ssh: connection timed out" — that's ssh's own
error, not ours, meaning the TCP connection itself was being dropped
while
the remote `elp launch`/`elp intent create` sat quiet for a long stretch
with no traffic, well before our own wait_for_finished(timeout_ms) would
have given up. Likely a NAT/firewall along the path treating an
apparently-idle connection as dead.
- Added `-o ServerAliveInterval=15 -o ServerAliveCountMax=8` to every
ssh/
rsync invocation, so the connection actively stays alive during a long
silent remote command instead of looking idle.
- Added an explicit `-o ConnectTimeout=30` (only bounds the initial
handshake, not the whole command).
- Replaced the scattered timeout magic numbers with named constants and
raised them: quick ops (staging a file, attaching one mount) 60s ->
2min;
provisioning (launch/intent create, which can legitimately take
minutes
for a slow cloud-init) 10-15min -> 30min; mount data rsync 30min ->
1hr.
fix: latest mdns fix
gabrielefronze
merged commit Sep 12, 2026
f4f213c
into
Elemento-Modular-Cloud:dev-llm-runtime
9 of 18 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.