File operations are pushed onto the device's command queue and run
asynchronously, but FileBrowser only re-listed the directory when dev,
path, or updateIndex changed -- and updateIndex was only ever bumped by
a breadcrumb click. The listing stayed stale after an upload, delete,
rename, or directory creation.
Watch the device's running flag and queue length, and re-list on the
busy -> idle transition so a multi-file delete refreshes once at the end
rather than per queued command. The refresh is silent: it skips the
loading flag so the current icons stay on screen instead of flashing the
spinner, and keeps the old listing if the re-fetch fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long CX II operations — listing a folder with many entries, downloading a
file of any size — failed with a reported `Busy`.
readPacket checksummed `transferred`, the byte count libusb returned, rather
than `completeLength`, the length the packet declares. The calculator can
hand back more bytes than the packet occupies (a 64-byte packet arriving as
65 was what showed up in tracing), and the stray byte failed the checksum.
The packet was dropped and so never acked, the calculator retransmitted the
same sequence number indefinitely, and the receive loop gave up after 40
reads. `transferred` was also wrong after the multi-chunk continuation loop,
where it holds only the final chunk's size — the reason large stream
transfers were unreliable.
Also add NNSE retransmission to packet_send_cx2. A lost packet or lost ack
was previously fatal, since the packet was written exactly once. Retries now
carry the "not the first try" bit (reqAck bit 3) that TI's own stack sets,
keeping the same seqno so the calculator can drop a duplicate. This path did
not trigger in testing — every packet acked on the first attempt once the
checksum was fixed — so its timeouts are deliberately generous to avoid
retransmitting during slow flash writes.
While here, zero `misc` and `unknown` instead of sending malloc garbage
inside a checksummed header, and handle allocation failure.
Verified against CX II firmware (0451:e022): `ls /` returns all 51 entries
across 8 runs, a 193 KB download is byte-identical across two runs, and
mkdir/rmdir round-trips.
Note the error was never actually Busy. The libnspire crate (0.2.3) matches
its positive NSPIRE_ERR_* constants against libnspire's negative return
codes, so every error falls through to the libusb table and is mislabelled;
-NSPIRE_ERR_INVALPKT and LIBUSB_ERROR_BUSY are both -6. That mismapping is
upstream in the crate and is not addressed here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app-shell commit (f45e893) added Home.vue, About.vue, and the router
but never staged App.vue, main.ts, or index.html. HEAD therefore still
contained Plan A's debug harness and a main.ts that wired neither the
router nor the device store — a fresh checkout would have built the
harness, not the real UI.
The working tree had the correct content all along, which is why the
running app was right and the verification (a grep against the working
tree, not the commit) passed.
- App.vue: harness replaced by router-view + tailwind import
- main.ts: router, globalProperties.$devices, provide(DEVICES_KEY)
- index.html: drop the scaffold's vite.svg favicon link
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
web/ declares @nuxtjs/tailwindcss, which pulls tailwindcss 1.9.6. npm
hoisted that to the root, where @tailwindcss/forms resolved it instead of
the 3.4.19 desktop needs, breaking the CSS build.
Task 9 worked around this by copying a package into node_modules, which
would not survive npm ci. Removing web from the workspaces array is the
durable fix and is consistent with the spec, which declares web knowingly
broken and out of scope for this port. Restore the entry when web is
migrated to Nuxt 3.
Verified from a clean install: root tailwindcss is 3.4.19 and the build
emits 33KB of real CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Router to vue-router 4 with hash history (custom protocol has no server
for path-based deep links). createApp replaces new Vue. The device store
is registered on globalProperties so n-link-core templates keep resolving
$devices, and also provided via DEVICES_KEY for <script setup> injection.
Removes Plan A's temporary harness and scaffold assets.
Also renames desktop/postcss.config.js and tailwind.config.js to .cjs:
with "type": "module" in desktop/package.json, Node treated their
`module.exports`/`require` as ES module syntax and errored. This was
latent since the Vue 3 foundation task because nothing previously
imported n-link-core/assets/tailwind.css for real; this task's App.vue
is the first to trigger postcss processing during the build.
Replaces the Vue 2 component-as-store (new Devices() extending Vue) with
reactive(). All ~10 $set/$delete calls are deleted -- Vue 3 proxy
reactivity tracks plain assignment and delete. Tauri 2 import paths
(@tauri-apps/api/core, @tauri-apps/plugin-dialog). runQueue/addToQueue
become module-scope helpers closing over the reactive state, since they
are internal to the store and not part of GenericDevices.
Adds a null guard in promptUploadFiles before iterating the dialog
result (the Vue 2 original would throw if the user cancelled the
dialog). Implements uploadFiles/uploadOsFile as intentional no-ops,
matching the original desktop store -- the desktop app resolves uploads
through a native path-based dialog instead of browser File objects.
npm run build only type-checks components something imports, so the three
tasks that ported components in isolation were never actually checked. A
temporary probe importing all five surfaced these:
- uploadNative declared its param as Event & {target: HTMLInputElement},
which is not assignable to the DOM's (payload: Event) => void handler.
Cast inside the function instead.
- info.os_extension.split('.').pop() is string | undefined but uploadOs
takes string. Extracted to an osExtension computed with a '' fallback.
Only CalcInfo was affected; FileIcon, FileView, DeviceSelect, and
DeviceQueue all passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
30 webpack require() calls replaced by a single eager import.meta.glob.
The extension-to-icon mapping is preserved exactly, restructured from a
switch chain into lookup tables. Fixes alt="name" rendering the literal
string 'name'.
Migrates n-link-core's Tailwind config from 1.x to 3.x (drops future/
variants/purge, swaps custom-forms for @tailwindcss/forms) and replaces
the Vue 2 'vue/types/vue' augmentation with Vue 3
ComponentCustomProperties. Deletes the Vue 2 shims.
Plan A verification found close_device timing out despite doing no I/O:
all commands were synchronous, Tauri runs sync commands on the main
thread, so a hung list_dir blocked every subsequent command.
The 11 libnspire-touching commands now spawn_blocking. enumerate and
close_device stay sync — neither performs a transfer. Return types become
concrete because impl Serialize cannot cross the async boundary. No
change to command names or payloads.
Ten tasks: async-command backend fix, Tailwind 3 + Vite foundation, the
8 n-link-core components, the device store, the app shell, verification.
Grounded in the real scaffold rather than assumptions. Reading the actual
sources surfaced three hazards missing from the earlier follow-on notes:
vue-async-computed (Vue 2 only, drives FileBrowser's listing and spinner),
30 require() calls in FileIcon, and @tailwindcss/custom-forms.
Tailwind 3 chosen over 4: the theme is a JS config with 16 @apply/theme()
uses in scoped SCSS, and 4's CSS-first rewrite would layer a second
migration onto the Vue one.
Task 1 fixes the command-serialization defect found during Plan A
verification: all commands were sync, so a hung list_dir blocked the whole
queue. It goes first because the UI is otherwise untestable on this
hardware.
Core components inject the store via an InjectionKey rather than importing
it, so n-link-core stays decoupled from the desktop implementation and the
web consumer remains portable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Correct the vendored libnspire-sys retry limit in spec and plan: the
actual constant in vendor/libnspire-sys/libnspire/src/cx2.cpp:391 is 30,
not 100. The '10 -> 100' claim originated in nlink-cli's own Cargo.toml
comment and was propagated into both documents unverified.
- Drop @tauri-apps/plugin-opener from desktop/package.json; the Rust plugin
and its capability entry were removed in Task 3, leaving this dangling.
- Reconcile desktop/package.json version 0.1.0 -> 0.1.6 to match Cargo.toml
and tauri.conf.json.
- Rewrite desktop/README.md, which still instructed users to
'sudo apt install webkit2gtk-4.0' -- the exact unavailable package this
port exists to move off. Now documents npm workspaces, the --legacy-peer-deps
requirement, the dist/-before-cargo gotcha, snap LD_LIBRARY_PATH breakage,
AppImage FUSE fallback, and the vendored patch.
Disclosure note: commit 4626858 also committed the previously untracked
nlink-cli/ and vendor/ trees (~5900 lines). Committing vendor/ is required --
the [patch.crates-io] override cannot resolve without it -- but the commit
message did not say so. Recorded here rather than rewriting branch history.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces Tauri scaffold placeholder icons with app-icon.png. Android/iOS
variants emitted by the icon tool are gitignored — this is a Linux-only
build per the plan's scope.
Verified: deb depends only on libwebkit2gtk-4.1-0 + libgtk-3-0, with no
webkit2gtk-4.0 or libssl1.1 reference — the failure this port exists to fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a throwaway App.vue that exercises enumerate/open_device/list_dir/
close_device over Tauri 2 IPC and logs addDevice/removeDevice/progress
events. open_device, list_dir, and close_device are wrapped with a 10s
client-side timeout each so a hang or Busy response from the CX II's
known firmware issue can't block the other calls from reporting;
enumerate only reads USB descriptors and is left untimed.
Installs @tauri-apps/plugin-dialog and @tauri-apps/plugin-shell per the
task-7 brief's Step 1. Manual GUI verification against a connected
device (brief Steps 3-4) is out of scope here and left to the user;
download_file/sha256 cross-check is intentionally omitted since any
libnspire transfer against this CX II is known to hang. Replaced by
the real UI in Plan B.
Reuses nlink-cli's already-migrated cli.rs rather than repeating the
clap 3-beta -> 4 migration. Also copies NOTICE.txt alongside cli.rs
since its include_str! path is relative to the file's own directory.
Hotplug registration moves from on_page_load to setup (runs once, so the
AtomicBool guard is dropped). v1 allowlist replaced by a capability file
granting dialog:allow-open and shell:allow-open. CSP from 0472908 carried
forward to app.security.csp; bundle targets narrowed to deb + appimage.
Also derives Clone on device::AddDevice: Tauri 2's Emitter::emit requires
Serialize + Clone (v1's did not), and the addDevice event payload was
missing the derive. No wire-format or IPC contract change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Window -> WebviewWindow, emit now requires the Emitter trait, and
hashbrown's drain_filter is replaced by an explicit collect-then-remove
on std HashMap. IPC contract unchanged.
Also derive Clone on ProgressUpdate (device.rs): Tauri 2's
Emitter::emit requires Serialize + Clone, unlike Tauri 1's
Window::emit which only required Serialize. Required for
progress_sender's window.emit("progress", ProgressUpdate { .. })
call to compile. No change to serialized shape or field names.
Splits the former 478-line main.rs into focused modules. Drops hashbrown
in favour of std HashMap (the drain_filter API it was used for was
renamed with changed semantics) and drops vendored libusb1-sys in favour
of the system library, matching nlink-cli.
Also removes the Task 2 scaffold's lib.rs/tauri-plugin-opener indirection
(the brief's module layout is a plain binary with no lib target) and drops
the now-dangling "opener:default" permission from capabilities/default.json,
which otherwise fails the Tauri build script with an unresolved permission
error.
Tauri 1.0.0-beta.8 links webkit2gtk-4.0, unavailable on Ubuntu 26.04.
Verified: builds and links libwebkit2gtk-4.1.so.0, libsoup-3.0.so.0,
libjavascriptcoregtk-4.1.so.0 with no webkit-4.0 or libssl.1.1 refs.
Workspace moved from yarn to npm; yarn.lock invalidated by this port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
n-link-cli ls / returns Busy then hangs (known CX II firmware issue,
owned by the user, out of scope here). Transfer-dependent gates are
downgraded to non-blocking; enumerate remains a full verification of the
IPC path since it reads USB descriptors only and does no libnspire
transfer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eight tasks from system-dep install through verified deb/appimage
bundles, with a webkit2gtk-4.1 go/no-go gate at Task 2 and n-link-cli
used as the verification oracle against the connected CX II.
Splits the spec's two phases into two plans; Plan B (frontend) is
written after Plan A is verified so it can be grounded in the real
scaffold output rather than assumptions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports desktop from Tauri 1.0.0-beta.8 (webkit2gtk-4.0, unavailable on
Ubuntu 26.04) to Tauri 2.x (webkit2gtk-4.1), alongside Vue 2 -> Vue 3 and
Vue CLI -> Vite. Wires in the locally patched libnspire-sys already proven
by nlink-cli.
Scoped to Linux (deb + appimage). The web/ package shares n-link-core and
is knowingly left broken, deferred to its own project.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>