23 Commits
Author SHA1 Message Date
Roman ZajicandCopilot Autofix powered by AI f6bca12270 Test/MCP preparation UI tests (#338)
* ci(tests): run shutdown-test in the PR gate and enforce the combined suite time budget

* test(harness): add shared fixtures (LGX generator, G-ERR/G-ALIVE/G-EXIT gates, xfail runner) and rea

* test(ui): assert welcome page state on first launch before any interaction (A1)

* fix(tests): retry the A1 greeting assertion so it cannot race the async launcherApps refresh

* ci(tests): tee the app log into BASECAMP_APP_LOG so the ui-tests G-ERR gate scans real output

* fix(tests): typename

* fix(tests): shutdown test to follow behavior from 3a73d33

* fix(tests): reduce comments

* fix: potential flakiness

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: replace full QML tree serialization

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: file URL construction

* fix: use bash explicitly

* test(qml): add spec §4.1 automation objectNames to QML sources

Add the stable automation handles enumerated in spec §4.1, following
the existing sidebar.app.<name> / confirmationDialog.<mode>.<button>
convention: welcomePage.installNow, appManager.{searchField,
category.<name>,emptyView}, appContextMenu root,
addApplicationDialog.{primaryButton,closeButton,errorText,
resolutionBanner}, settings.searchField,
pluginInterface.{call.<method>,result,back}, repositories.{urlField,
addButton,refreshButton,errorBanner,errorDismiss,row.<url>,
removeConfirm.{confirm,cancel}}, and sidebar.buildLabel.

workspace.dock.<name> is deliberately not applied: the dock root's
objectName is pinned to the bare module name by
workspace_area_test.cpp and needs a coordinated rename (recorded as
a finding).

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-26 16:52:28 +08:00
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 9c3d023062 feat(shell): put the UI shell behind IShellHost, and order its shutdown
The shell held the host's objects: MainContainer owned MainUIBackend, took a
LogosAPI* and a QtLogosCore*, and connected to backend signals by concrete
type. Nothing stopped it minting identities or reading the token store.

Three headers in app/interfaces/, on the include path both targets share, so
there is exactly one copy and source drift is impossible:

  * IShellHost     -- 8 operations the shell may perform
  * IShellObserver -- 5 notifications the host may deliver
  * IShellView     -- how the host builds and tears down the shell

Only QObject*, QWidget* and Qt value types cross. MainContainer holds one
IShellHost* and nothing else; QML reaches the backend as an opaque QObject*
via backendObject(), resolved through the metaobject, so no host C++ type has
to be nameable by the shell.

Ownership inverts: Window owns MainUIBackend, ShellHostAdapter and
MainShellView; the shell borrows. Teardown stops being a consequence of
construction order and becomes a stated contract:

  1. beginShutdown() unmounts in-process UI plugin widgets WHILE the shell's
     tree is intact -- they are docked inside it
  2. destroyShell() detaches the observer, then deletes the shell
  3. the backend goes, tearing down Package -> UIPlugin -> Core
  4. main() destroys the core facade

Every observer forward is null-guarded: PluginLoader dispatches through
QTimer::singleShot(0, ...) and a 30s ViewModuleHost timeout, so a callback can
land after teardown starts, and QPointer cannot help -- IShellObserver is not
a QObject. UIPluginManager's plugin-widget maps become QPointer for the
mirror-image reason: those widgets are docked inside the shell, so its Qt
parent can destroy them without going through unloadUiModule.

IComponent is untouched and still serves the third-party legacy widget
plugins PluginLoader loads.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 13665a5e34 feat(host-core): adopt the SDK core facade, and delete both C ABI mirrors
CoreModuleManager and main.cpp each carried a hand-written `extern "C"` mirror
of liblogos' core-management ABI -- two blocks declaring the same symbols in
one image, an ODR hazard with no diagnostic. Both are replaced by
logos::qt::QtLogosCore over logos::host::LogosCore, which owns the
char*/char** marshalling, the `delete[]`-not-`free()` rule, and the pre-start
ordering constraint -- the last as constructor arguments, so the illegal order
stops being expressible.

Four things about the stats path that fail INVISIBLY, all preserved:
  * "cpu"/"memory" stay 1-decimal STRINGS -- QML and ModuleInstanceModel bind
    those names, so the facade's cpuPercent/memoryMb spelling stops here
  * allStats() is called ONCE per tick; each moduleStats(name) repeats the
    full C call and parse, and the snapshot walks every known module
  * the three-way back-compat fallbacks are kept for older runtimes
  * memoryMb (double), never memoryBytes

The cpp-sdk lock moves for that last point: the pin predated the rename, and
that pair does not compile -- qt-sdk's header reads s.memoryMb while the
pinned cpp-sdk struct still called it memoryBytes.

Also drops a QTimer in main.cpp that was started and stopped with no
connect() at all.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 7851bf9f46 feat(host): per-plugin identities, an opt-in access policy, and the source split
Each loaded plugin -- including pure-QML ones -- gets its own LogosAPI identity
rather than sharing the host's, so a plugin's calls are attributable and can be
refused independently. The host-services grant is wired through to
capability_module, and its trust root is guarded on an OUTCOME rather than a
log line.

Inter-module access policy stays OFF by default: enforce mode's derived
deny-by-default gates every ui_qml app's calls to its own backend module,
because UI plugins load out-of-process and are not tracked as dependents in the
core ModuleRegistry. Operators opt in per launch with --access-policy enforce
or LOGOS_ACCESS_POLICY.

Takes the Qt host runtime from logos-plugin-qt rather than logos-qt-sdk, which
keeps only the Qt<->lp seam headers, and moves logos-protocol onto the rev that
split host needs. On Windows logos_core must come LAST on the link line: GNU ld
resolves an archive left to right, so the view runtime's references have to be
undefined already when it reaches the import library.

Separates the two source trees -- app/ is the host, src/ is the UI shell -- and
brings the CI onto setup-nix-cache-action. Merges master.
2026-08-22 16:42:13 -03:00
Dario LipicarandClaude Opus 5 aa5884c673 feat(windows): cross-build Basecamp — plus startup and window fixes for every platform (#323)
* feat(windows): wire Basecamp for the x86_64-windows target

First slice of Stage 4. Basecamp now evaluates for the Windows
pseudo-system up to its first genuinely unported dependency
(logos-design-system); native aarch64-darwin and x86_64-linux
evaluation is unchanged.

Four blockers cleared, found by evaluating and fixing in turn:

1. flake.nix used a hardcoded `systems` list, so packages.x86_64-windows
   did not exist. Now routes through logos-nix.lib.forAllTargets, which
   keys the cross target as a pseudo-system — that is what lets the 34
   `dep.packages.${system}.x` interpolations stay untouched. Bundlers
   (nix-bundle-logos-module-install, nix-bundle-dir) are keyed by
   buildSystem instead: they RUN on the builder, so taking them from the
   target set would hand it a PE it cannot execute.

2. meta.platforms was platforms.unix.

3. krb5 does not cross-evaluate: it carries a host-platform bash, so the
   splice fails with "Refusing to evaluate package 'bash-5.3p9'" — an
   error naming bash, several levels from the cause. Guarded in both
   common.buildInputs and app.nix's qtLibPath.

4. qtwebview propagates qtwebengine — a full Chromium — which fails to
   cross-evaluate on cups, three levels from the cause. It is also
   entirely unused: no C++ include, no `import QtWebView` in any QML,
   and app/CMakeLists.txt's find_package COMPONENTS list omits WebView.
   The only surviving mention is a stale line in docs/project.md.

krb5 and qtwebview are GUARDED rather than deleted, so this stays a
Windows-only change. Both look like dead weight on Unix as well, and
dropping them there would shrink every bundle — worth doing separately,
with its own testing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): drop the QML inspector on Windows

logos-qt-mcp is the QML inspector behind the UI test harness. It has no
Windows target and is not needed to run the app -- nix/app.nix already
accepts `logosQtMcp ? null` and gates the inspector on it -- so Windows
builds go without it, and the inspector-dependent outputs are simply
absent from the Windows package set.

With this, logos-basecamp EVALUATES for x86_64-windows end to end; native
aarch64-darwin and x86_64-linux are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): cross-build LogosBasecamp.exe

Basecamp now produces a real Windows executable. Five blockers, each
found by building and each of a kind already seen in this port.

1. abseil-cpp does not cross-compile (absl/base/internal/
   thread_identity.cc includes <pthread.h>, absent on mingw+mcfgthread).
   It is dead weight -- "absl" appears nowhere in the sources or CMake,
   only in nix files and the README. Guarded here; also removed from
   logos-package-manager-ui's metadata, which pulled it in separately.

2. installedModules cut to [] on Windows. The install path runs each
   module through nix-bundle-logos-module-install -> nix-bundle-lgx ->
   nix-bundle-dir, and nix-bundle-dir is ELF/Mach-O only. This keeps the
   bundler chain, lgpm and four module repos off the critical path.

3. The hand-rolled configurePhase never passed $cmakeFlags, so
   -DCMAKE_SYSTEM_NAME=Windows was dropped. The symptom was nowhere near
   the cause: FindThreads probed for pthreads instead of Win32 threads and
   Qt6Config reported "Qt6 could not be found because dependency Threads
   could not be found".

4. Qt's host-TOOL packages were unreachable. app.nix builds its own cmake
   line and never inherited common.cmakeFlags, so adding the flags there
   was not enough -- they had to go on the invocation too. Symptom again
   misleading: 'Failed to find required Qt component "RemoteObjects"',
   when the TARGET Qt6RemoteObjects is present and it is
   Qt6RemoteObjectsTools, the host package, that is missing.

5. Two unsuffixed-name defects, the class that has now appeared eleven
   times. app/CMakeLists.txt copied `logos_host` without
   CMAKE_EXECUTABLE_SUFFIX, and because it is a POST_BUILD step the
   failure read "FAILED: LogosBasecamp.exe" with no undefined references
   -- the real message was the "Error copying file" line above it. Then
   the installPhase's `if [ -f build/LogosBasecamp ]` had no else-branch,
   so the mingw build installed NOTHING, exited 0, and produced an output
   whose bin/, lib/, modules/ and plugins/ were all empty. It now probes
   both spellings and hard-fails with a directory listing.

Also: ENABLE_QML_INSPECTOR is now gated on logosQtMcp != null rather than
on enableInspector alone -- the inspector cannot be on without it -- and
Windows installs the binary directly instead of behind a /bin/sh wrapper
that could not run there.

NOT YET RUN on Windows; that needs the Qt plugin/QML staging proven in
the design-system milestone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): main_ui configures and compiles under cross

Two fixes, both of kinds already seen in this port.

The code generator must RUN on the builder, so it now comes from the
BUILD system via a new logosSdkBuild binding. This is a SPLIT, not a
swap: the same cpp-sdk output carries both the generator binary AND the
target headers/CMake package, so logosSdk stays for
-DLOGOS_CPP_SDK_ROOT. Getting it backwards succeeds natively and, under
cross, puts a PE on the builder's PATH -- the symptom was
"logos-cpp-generator: command not found".

main-ui.nix has its own hand-rolled configurePhase which, like app.nix's,
never passed $cmakeFlags -- so -DCMAKE_SYSTEM_NAME=Windows was dropped
and FindThreads probed for pthreads, surfacing three levels away as
"Qt6 could not be found because dependency Threads could not be found".
It now passes $cmakeFlags and Qt's host-TOOL package paths.

main_ui now configures and compiles. It does NOT yet link: the qt-sdk
static library is pulled in twice, giving "multiple definition of
LogosAPI::LogosAPI". That is a link-graph question, not a Windows
spelling issue, and is the next thing to solve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): name the main_ui plugin .dll, and link it

src/CMakeLists.txt set the plugin SUFFIX with `if(APPLE) ".dylib"
else() ".so"` and no Windows branch, so the cross build emitted
main_ui.so -- a genuine PE carrying a Unix extension. app/window.cpp
looks for ".dll" under Q_OS_WIN, so it would never have been found and
Basecamp would have gone on reporting "No main UI" with nothing
obviously wrong. Windows is now tested before the else-branch, and
nix/main-ui.nix accepts the .dll spelling too.

With the liblogos_core export fix, main_ui links and installs as
main_ui.dll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: don't block startup on modules that are not loaded

PackageCoordinator guarded its startup IPC with
`if (!client || !client->isConnected()) return;`, which never fired --
see the logos-protocol fix. Past that dead guard it made ~19 blocking
calls (4 directory setters, 8 package_manager subscriptions, 1
package_downloader) against modules that were absent, each waiting 20 s
twice over. All of it runs on the GUI thread inside the Window
constructor (window.cpp invokes createWidget with Qt::DirectConnection),
and main.cpp calls show() only afterwards -- so the window simply did not
appear for ~7 minutes.

Ask the question in-process instead: logos_core_get_loaded_modules, via
the CoreModuleManager this class already holds. No IPC, no timeout. The
same guard already ships in logos-logoscore-cli's daemon, which skips the
identical setEmbeddedModulesDirectory block and reports that package
commands are unavailable. Basecamp already logged "Failed to load
package_manager module by default" roughly 40 s before the first block --
it knew, and ignored it.

The subscriptions are re-armed on CoreModuleManager::coreModulesChanged,
so a module installed later in the session still gets wired up; without
that this would trade a 7-minute stall for a silently non-functional
Modules view, which is the worse bug. Both subscribe paths are
idempotent, and the "not loaded" warning is emitted once rather than on
every stats-timer tick.

Verified on Windows: the guard now fires, the transport warns before the
doomed wait, and Basecamp's real UI renders -- sidebar, Logos mark and
Welcome page -- instead of a blank window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: guard the remaining package_manager startup calls

fetchUiPluginMetadata() and refreshDependencyInfo() checked only
`!m_logosAPI`, so they still blocked ~20 s acquiring a token for an
absent package_manager after the subscribe paths were guarded. That was
the last blocking wait at startup, and the transport's new
"will block up to 20000 ms" warning is what pointed at it.

fetchUiPluginMetadata reuses its existing metadata-unavailable branch
rather than inventing a new one -- it clears m_appsLoading and emits
uiModulesChanged, so the UI settles instead of sitting in a loading state
for the timeout. refreshRepositories needed no change: it guards on
isConnected(), which now tells the truth.

Measured on Windows with no modules installed: "will block up to" 0,
"Timeout waiting for replica" 0, "Requesting object" 0. The UI renders
identically -- sidebar, Logos mark, Welcome page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): resolve UI plugins' private DLLs from their own directory

The logos-module chokepoint fix covers LogosModule::loadFromPath, which
is every process-isolated module. Basecamp constructs its own
QPluginLoaders and was not covered: PluginLoader's background pre-load
and finishCppPluginLoad for UI plugins, and window.cpp for main_ui.

Each lives in its own directory (plugins/<name>/), so anything vendored
beside it is invisible to Windows' loader, which searches the
EXECUTABLE's directory rather than the importing DLL's. This works today
only because every DLL happens to be staged next to the exe; a plugin
with a private dependency would fail with "The specified module could not
be found", naming the plugin rather than the dependency.

The references are deliberately not released: these plugins stay resident
for the process lifetime (QPluginLoader's destructor does not unload).
No-op off Windows, so no platform guards at the call sites.

Cross-builds and native build both green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(windows): key the install bundlers by target

nix-bundle-logos-module-install now does its own host/target split
internally, so keying the whole bundler by buildSystem is no longer
right -- that is what made it derive the .lgx variant name and library
extension from the build platform.

installedModules stays [] on Windows for now: with the labelling fixed,
lgpm correctly refuses to install a windows-x86_64 package on a Linux
builder, and cross-installation needs a platform override that lgpm does
not have yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): ship the pre-installed modules

installedModules is no longer cut on Windows. The cross-install path now
works end to end: capability_module, package_manager and
package_downloader are laid out under modules/, main_ui and
package_manager_ui under plugins/, each a real PE with its own DLL
closure and a variant file reading windows-x86_64-dev.

It took a host/target split at every layer, and each failed somewhere
unrelated to itself: the .lgx bundler keyed by target (it decides the
variant name and library extension), lgpm from the build system (it runs
there), a --platform override for cross-installation, and a Nix-double
to lgpm-variant translation.

Verified on real Windows: capability_module and package_manager load,
spawn their hosts, and answer IPC over named pipes -- the log shows
ModuleProxy rejecting unauthorized calls, which is the token flow
talking, not a Windows failure.

KNOWN GAP, and it fails loudly: package_downloader does not load --
"Cannot load library ... package_downloader_plugin.dll: The specified
module could not be found". Its external library
libpackage_downloader_lib.dll needs libcurl-4.dll, which is in neither
the module directory nor bin/. LogosModule.cmake copies an external
library's DLL but not that DLL's own dependencies, so the closure must be
walked to a fixpoint there too -- the same lesson as the Qt plugin and
bundle closures. Strictly better than shipping no modules at all, and the
failure names the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): import the shared runtime instead of linking it

LogosBasecamp.exe and main_ui.dll now compile with LOGOS_SHARED_USE_DLL
and link an emptied logos-qt-sdk archive, so TokenManager, LogosAPI and
LogosAPIClient come from liblogos_core.dll. Before this each image linked
its own copy and therefore its own function-local statics: the host saved
a capability token into its TokenManager, main_ui read its own empty one,
and every cross-module call was refused.

Measured on real Windows across three runs: "rejecting unauthorized call"
29 -> 0, and "No token found" 29 -> 0 replaced by nine "Found token for
module". All modules still load; no missing-symbol or duplicate-symbol
diagnostics.

NOT fixed by this, and not caused by it: the package_manager_ui icon
still does not appear in the sidebar. The pre-change run had the same
zero ui-host spawns. Sidebar entries come from
PackageCoordinator::getInstalledUiPluginsAsync, which reads
package_manager's installed-package registry -- empty in this payload, so
the list is empty regardless of what is on disk in plugins/. That is a
separate unmet precondition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: never open a window larger than the screen

setupUi asked for a fixed 1600x900 and nothing anywhere in this repo consulted
the screen -- the <QScreen> include at the top of window.cpp had been unused.
On a display smaller than that the window opens with its right and bottom edges
off-screen, and on Windows it cannot be dragged back into view, so whatever
hangs off is permanently unreachable: in practice the sidebar's bottom-anchored
system buttons and the version label.

Two halves, because one is not enough:

* setupUi clamps the requested size to availableGeometry, so an oversized
  window is never mapped in the first place.
* showEvent re-checks once. resize() sets the CLIENT size while
  availableGeometry() bounds the FRAME, and the decoration margins are not
  knowable until the platform window exists -- measured, the clamped client
  filled the work area exactly and the frame still hung 72px below it.

Both are skipped under the offscreen platform (QOffscreenScreen hardcodes its
geometry) so headless UI doctests keep their full-size window and qt-mcp's
scene-position clicks still land.

Three things that look like they could be simpler and cannot, each measured:

* The showEvent check compares SIZES, not rectangles. QRect::contains() is the
  obvious formulation and misfires on both platforms: on Windows
  frameGeometry() comes from GetWindowRect, which includes DWM's invisible
  resize border (~13px/side at 192dpi), so a correctly placed window never
  reports as contained and this would run on every launch; on macOS Qt reports
  a frame whose title bar sits above availableGeometry's origin, and acting on
  that moved the window 66px down at launch on a platform where nothing is
  broken.
* The shrink is bounded by the current size. Without boundedTo, a window that
  is merely too TALL is also stretched to the full available WIDTH -- observed
  on this path: 1600 logical wide silently became 1715.
* The clamp is to availableGeometry exactly, not to a fraction of it. Anything
  less is not a no-op on displays that fit the design size, and would undo the
  widening documented at the resize() call (1600 was chosen so the Package
  Manager's Action and Description columns are not clipped).

Measured on Windows (real hardware, DPI-aware capture, visible frame via
DWMWA_EXTENDED_FRAME_BOUNDS against a 3456x1826 work area):

  normal session      bottom overflow 45px -> 2px (rounding), width still 1600
  small desktop       ~550px of width off-screen -> right 14px, bottom 2px
  below app minimum   bottom 126px -> 33px (Basecamp's own 800x600 floor)

Verified a no-op elsewhere by running the identical logic against Qt 6.11.1 on
macOS: constructor clamp no-op, showEvent reports FITS, size and position
unchanged; and under QT_QPA_PLATFORM=offscreen both paths are skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: re-fit the window when the screen shrinks under it

The launch clamp (76e0f6d) only runs once. An RDP session with
/dynamic-resolution, or a taskbar appearing, shrinks the desktop under a running
window and nothing reacted -- the window was left overflowing, with the
sidebar's bottom-anchored system buttons under the taskbar and unreachable.

Watches availableGeometryChanged on the screen the window is on, coalesced
through a 150ms settle timer, and re-fits. Measured on real Windows, same
binary pair and run conditions, work area shrinking 1922 -> 1826 physical px:

  shipped   overflow (3,3) -> (4,111)   window did NOT shrink   NO-REFIT
  re-fit    overflow (3,3) -> (3,2)     window shrank           REFIT-WORKS

An 8-agent design pass was REFUTED 3/3 by its own reviewers and this is the
corrected version, not that design. Three of its defects were real:

* THE RATCHET. It fitted to min(previous, available) on every event -- a
  monotone non-increasing map, so the window converges on the smallest work area
  seen all session and never grows back. A taskbar that auto-hides and
  reappears, or an RDP window dragged smaller and back, would have shrunk
  Basecamp permanently. Fixed by bounding against m_desiredSize: the launch
  design size, replaced by any resize the USER performs, so the window grows
  back but never past what was asked for.
* NO isVisible() GATE. closeEvent hides to the tray on EVERY platform and hide()
  does not set Qt::WindowMinimized, so a window-state gate does not cover it.
  The fit would have run and latched on a hidden window whose frame geometry is
  meaningless.
* A QRect LATCH that could not distinguish "the screen shrank under the window"
  from "the window moved to another monitor". Dropped entirely: the
  `target == size()` early return is both the idempotence and the
  non-recursion, so there is no latch to fall out of sync -- and no need for the
  design's re-entrancy flag, which a reviewer showed could never be true on
  entry.

Three things kept from that design because they beat the obvious choice:
screenChanged re-points the watch but never fits (Qt migrates windows onto a
1024x768 lock screen on RDP disconnect, and fitting there would shrink with no
way back); the settle timer, because the fit shrinks and chasing a drag's
intermediates would latch its smallest frame; and connections held by handle
with a QPointer<QScreen>, because a session lock adds and then DELETES a screen.

Every guard lives inside fitFrameToAvailableGeometry() rather than in the
callers, so all three entry points are covered by construction -- a reviewer
caught that the design left the scheduler unguarded while changeEvent called it
on every state change.

Two bugs found in this implementation while writing it: QPointer<QScreen> needs
QScreen COMPLETE (QPointer static_asserts QObject derivation), caught by the
cross build; and resizeEvent recorded the MAXIMIZED size as the user's intent,
so a later un-maximize would try to grow the window to full-screen size. Also
replaced `if (!m_screenChangedConnection)` with an explicit QPointer<QWindow>
comparison: Qt documents Connection::operator bool only as "the connect() call
succeeded" and says nothing about sender lifetime, so a stale-but-truthy
connection would have silently stopped re-pointing the watch.

Verified a no-op elsewhere by running the shipped function body against Qt
6.11.1 on macOS: constructor clamp no-op, fit reports FITS, size and position
unchanged; and under QT_QPA_PLATFORM=offscreen both paths are skipped, so
headless UI doctests keep their full-size window.

NOT verified: whether an RDP resolution change arrives as
availableGeometryChanged on the same screen rather than a screen remove/add,
which is deliberately not fitted on. The work-area path is proven; that one is
inferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(windows): ship the portable Basecamp with its modules pre-installed

The distributed build cut installedModules to [] on Windows, and skipped
nix-bundle-dir was never the reason -- the comment claimed bundle.sh
would emit an empty payload, which is true, but the conclusion should
have been "don't run bundle.sh on Windows", not "ship no modules".

A PE needs no relocation: its import table carries DLL base names only
and Windows searches the executable's own directory first, so the build
output is already position-independent, and win-dll-link.sh has staged
the closure into bin/. nix-bundle-lgx reached the same conclusion for
module payloads in d539a3f; this applies it to the app bundle.

Without this there is no portable Windows Basecamp with a working
package manager, and therefore no way to install anything from a
catalog: platformVariantsToTry() appends "-dev" to every variant unless
LGPM_PORTABLE_BUILD is set, so the dev build asks for
`windows-x86_64-dev` while a published package is `windows-x86_64` and
every catalog row reports Not Available.

Native builds are unchanged by construction -- bundleFor is dirBundler
off Windows.

* fix(windows): app.nix copied no shared libraries at all

The liblogos shared-library loop globbed *.dylib and *.so only. On Windows
it matched nothing, and because each candidate is guarded by `[ -f "$f" ]`
with a trailing `|| true`, it copied zero files and exited 0.

Thirteen DLLs sit in that same lib/ -- liblogos_core, libpackage_manager_lib,
liblgx, icuuc76, icudt76, libsodium-26 and the mingw runtime -- and
LogosBasecamp.exe imports liblogos_core.dll DIRECTLY. The result is
0xC0000135 (STATUS_DLL_NOT_FOUND) before main(): no Qt error, no stderr, no
output whatsoever. It has only ever run on Windows because those DLLs were
hand-staged into the payload.

Adds *.dll to the glob, sends DLLs to bin/ rather than lib/ (Windows searches
the executable's own directory, has no rpath, and win-dll-link.sh only
processes $out/bin), and makes copying zero libraries a hard error -- this
loop exiting 0 having done nothing is the whole defect.

Also corrects the comment on bundleFor. It claimed skipping nix-bundle-dir on
Windows was right because a PE needs no relocation. That conflates the
bundler's two jobs: relocation (genuinely unnecessary on PE) and Qt plugin /
QML / qt.conf staging (required on every platform, since those DLLs are
LoadLibrary'd and appear in no import table). Skipping it yields a bundle with
no qt.conf and no QPA plugin. The comment now says so and names the real fix.

* fix(windows): stage liblogos' own dependency DLLs, and prove the closure

461ddcd copied ${logosLiblogos}/lib into bin/, which was necessary but not
sufficient. liblogos_core.dll and logos_host.exe each import libspdlog.dll and
libfmt.dll, and those two live in liblogos' BIN, not its lib/ -- nixpkgs'
win-dll-link.sh walked logos_host.exe's imports and staged the closure there.
spdlog is a buildInput of liblogos, not of basecamp, so basecamp's own
win-dll-link pass could not resolve them either.

Measured on the built bundle with a PE-capable objdump, fixpoint over 52 seed
PEs: with lib/ copied and bin/ not, libspdlog.dll and libfmt.dll were the only
non-OS names left unresolved for LogosBasecamp.exe. That is still 0xC0000135
before main() -- the exact symptom copying lib/ was meant to cure. Mirrors what
logos-package-manager/nix/lib.nix already does with liblgx's closure.

This runs in postFixup, not installPhase, and the ordering is load-bearing:
win-dll-link.sh registers _linkDLLs in fixupOutputHooks, which run BEFORE
postFixup. Doing it in installPhase makes the already-present skip vacuous, so
Qt6Core/Qt6Network/Qt6RemoteObjects get copied out of liblogos as real files
while Qt6Gui/Qt6Widgets stay symlinked into basecamp's own qtbase -- a bin/
with a MIXED Qt in it the moment those pins diverge. Verified: in postFixup the
loop stages exactly 2 files and every Qt DLL stays a symlink.

The check asserts the invariant rather than the mechanism: every DLL that
liblogos_core.dll imports AND that liblogos itself ships must resolve next to
the executable. Names liblogos does not ship are OS DLLs, so no hand-maintained
system-DLL list can rot. A zero import count is treated as a measurement
failure (objdump with no PE target), never as a pass. Both failure paths were
observed firing, not assumed.

Native Linux is unchanged: same file set, byte-identical staged .so files.

* fix(windows): drive the bundle's DLL import closure to a fixpoint

$out/modules/<m>/ and $out/plugins/<p>/ were never walked at all.

nixpkgs' win-dll-link.sh registers _linkDLLs in fixupOutputHooks, and
that entry point only ever processes $prefix/bin. modules/ and plugins/
are filled in installPhase from .lgx payloads and UI-plugin outputs -- in
directories the hook does not look at -- so nothing ever read their
import tables. Measured on this bundle before this commit: main_ui.dll
imports Qt6Qml.dll, Qt6QuickControls2.dll and Qt6QuickWidgets.dll and not
one of them was in plugins/main_ui/ or in bin/.

One pass would not have been enough either: you cannot read the imports
of a DLL that is not there yet. The sweep needs five rounds here, and
round 4 is not decoration -- Qt6QmlWorkerScript.dll is reachable only
through Qt6QmlMeta.dll, which round 3 staged.

So: index every .dll in the build closure (closureInfo over the same
roots as buildInputs -- a PE embeds no store paths, so this derivation's
own references are useless for the purpose), then walk bin/, lib/,
modules/* and plugins/* to a fixpoint, staging what is missing into bin/
and FAILING on anything the closure cannot provide. Result: 9 DLLs staged
over 5 rounds, and every non-system import in the bundle now resolves.

This is a hard error and not a warning because the failure it prevents is
totally silent: a missing DLL is 0xC0000135 before main() runs, or
ERROR_MOD_NOT_FOUND blamed on the plugin, with no Qt message and no
stderr. It exits 0 at build time every time.

It is also the only place the windowsHostLibs list in nix-bundle-lgx can
be falsified. That list DELETES DLLs from every .lgx payload on the claim
that the host ships them; nothing there can check the claim, and one
wrong entry (libiconv-2.dll, reached via libcurl-4 -> libidn2-0) shipped
a package_downloader that could not load on real Windows. A/B: with that
entry restored, this build now fails and names the DLL and its three
importers instead of producing a broken bundle.

Two anti-vacuity guards, since a wrong answer here always looks like
success: an empty DLL index is fatal, and reading zero imports across the
whole bundle is treated as a measurement failure (an objdump without a PE
target prints nothing, and then every import "resolves").

Also fixes a silent no-op next door: `if ls "$sdk/lib/"liblogos_sdk.*`
runs with nullglob set, so an unmatched glob left `ls` with NO arguments,
which listed the working directory and succeeded -- after which
`cp -L "$out/lib/"` ran with a single argument and printed
"cp: missing destination file operand", guarded by `|| true`. Test each
candidate instead. This is the only change outside the isWindows guard;
the native output tree is byte-identical apart from self-referential
store paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): the import gate could not see 12 of its own roots

Three holes, all silent, all in the check that exists to catch silent holes.

`_pe_roots` used `find ... -type f`, which does not match a SYMLINK -- and 12
of the 36 PE entries in $out/bin are relative symlinks created by nixpkgs'
win-dll-link.sh (Qt6Core, Qt6Gui, Qt6Widgets, Qt6Network, Qt6RemoteObjects,
libcrypto, libssl, libzstd, libb2, libpng16, libpcre2, libdouble-conversion).
Their import tables were never read. Four non-system names in this tree are
imported only by those roots, three of them entries on nix-bundle-lgx's
windowsHostLibs -- the list this gate is the only thing capable of falsifying.
`find -L` makes -type f test the target. Measured on the real bundle: root set
105 -> 117, exactly the 12 that were invisible.

Every branch also carried `-maxdepth 1`, so a PE below the first level of lib/,
modules/<m>/ or plugins/<p>/ was skipped with no message. Nothing in the current
bundle sits there, but it was demonstrated by putting one there: the gate stayed
at rc=0 while a full-depth sweep found 8 unresolved imports. Depth limit dropped.

A dangling symlink is invisible to `find -L -type f` and unopenable at runtime,
so it is now a hard error rather than a quiet omission from the root set.

And the success line claimed "Every non-system PE import in bin/, lib/, modules/
and plugins/ resolves" while checking neither the symlinks nor anything deeper
than depth 1. Since it was the only output on the pass path it read as proof of
full coverage. It now states the size of what it checked:

  PE import closure verified: 117 root(s), 7141 import(s) read, 0 unresolved.

* fix: give installing the same IPC budget as downloading

Downloading had a five-minute deadline (kDownloadIpcDeadlineMs) while
installing was left on the 20 s default from logos_mode.h — backwards.
Downloading is network-bound and retryable; installing is disk-bound over a
payload package_manager reads, gunzips and tar-parses three times and
Merkle-hashes twice, then extracts and copies. Measured at ~1 s on a warm dev
box and unbounded on a slow disk, a large package, or a machine whose
antivirus scans each DLL as it lands.

Blowing the deadline cancels none of that work: the files still install and
only the reply is abandoned, so the user is told the install failed when it
did not — which is exactly what a Windows install looked like while a separate
read-only-payload bug was making the reply never arrive at all.

Also names the empty-reply case instead of reporting the generic
"Installation failed". A dropped or timed-out call arrives here as a
default-constructed QVariantMap because installPluginAsync's callback has no
error slot; every real reply carries "path", so an empty map is never a
genuine answer. That inference is INTERIM: logos-cpp-sdk master f3369fa (#132)
already provides installPluginAsyncResult with logos::AsyncResult<T> carrying
the real CallError, and both call sites should switch to it once
feat/windows-cross picks that up — it is 12 commits behind master today. The
comments say so at both sites. f3369fa did not change the async default
deadline, so this Timeout is required either way.

* fix: tell a dropped install reply apart from a failed install

Both call sites used installPluginAsync, whose callback receives a bare
QVariantMap — so a transport failure and a provider that legitimately returned
an empty map are the same thing to the caller. A timed-out call therefore
surfaced as the generic "Installation failed", pointing the user at a package
that was very likely fine, and on Windows that is exactly what a separate
read-only-payload bug produced: every file installed, no reply came back, and
the row went red.

Switches to installPluginAsyncResult, whose callback takes
logos::AsyncResult<QVariantMap> and carries the value and the error together.
Transport failure is now checked FIRST, before `value` is read at all — on a
timeout `value` is default-constructed, and interpreting it as an install
verdict is the precise mistake this channel exists to remove.

The message says the deadline expired and that the package may in fact be
installed, because blowing the deadline cancels nothing already underway.

This replaces an interim heuristic that inferred a dropped reply from an empty
map. The inference was sound — a real reply always carries "path" — but it was
an inference. The real channel arrived on this branch with logos-cpp-sdk
f3369fa (#132, "async callers can see the error, sync callers can set a
deadline"), picked up by the master merge; the comments left at both sites
named it as the thing to switch to.

Builds for x86_64-linux and x86_64-windows in both repos.

* chore(deps): re-pin the chain to its merged revs

Levels 1-8 of the Windows chain are merged.  This branch was locked to
pre-merge revs of every one of them, so it could only evaluate against the
unmerged branches.

Level 9; the workspace re-pin follows once this lands.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:47:30 -03:00
Khushboo Mehta 25300f0e90 fix: fix the show/hide behavior for basecamp after its minimized 2026-07-31 13:16:18 +00:00
Khushboo Mehta 3a73d33824 chore: revert x on linux closes app 2026-07-30 14:58:49 +00:00
Khushboo Mehta f8dddc3456 chore: remove check logs in smoke test 2026-07-27 14:50:49 +00:00
Khushboo Mehta aa6bd126b9 feat: fix Cmd+Q on macos and Linux behavior expected as well 2026-07-27 14:50:49 +00:00
Khushboo Mehta c2702402cf fix: bring back tooltip on side bar 2026-07-17 14:21:00 +00:00
Dario Gabriel LipicarandClaude Opus 4.8 2a2aec2478 fix: widen default launch window so Package Manager columns are visible
The default 1024x768 launch size left the content area at ~912px, but the
Package Manager view needs ~1300px (category sidebar 200 + table min 984 +
margins/spacing) to show the table's Action and Description columns with the
details panel closed (the default no-selection view). At 1024 those rightmost
columns were clipped off the right edge. Bump the default to 1600x900 (~1504px
content, ~200px of slack), which shows the full table comfortably. The window
minimum stays 800x600 (MainContainer) so it can still be resized down; there is
no geometry persistence, so the new default applies on every launch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:39:06 -03:00
Iuri Matias 312a1dfb28 replace all Logos App references 2026-05-25 13:03:20 -04:00
Dario Lipicar 113b67c733 leverage package-manager-module for module/ui-plugin detection (#110) 2026-03-27 10:57:53 -03:00
Khushboo Mehta 17ef99cb28 chore: rename repo to logos-basecamp 2026-03-18 14:36:41 +01:00
Dario Lipicar ccb4cdba1a properly handle portable modules (#71)
* properly handle portable modules

* preinstall modules on startup instead of bundling them at build time

* get icons from manifest

* consistent user dir

* revert to main branch of dependencies

* add release derivations

* add appimage release job

* fix app icon on Linux
2026-02-26 11:05:34 -03:00
Khushboo Mehta d4281e96be chore: review fixes 2026-02-16 15:43:43 +01:00
Khushboo Mehta 2f199aa04d feat: Making macos traffic buttons to be overlayed on top of app and alligning app for this to look okay 2026-02-16 15:43:40 +01:00
Iuri Matias d893b0b69e support installing plugins inside folders 2026-01-23 11:14:48 -05:00
Iuri Matias 813ad1269b extract package manager ui to its own plugin 2025-12-09 10:54:41 -05:00
Iuri Matias 827dc485d2 support systray 2025-11-25 11:36:34 -05:00
Iuri Matias ebf9cf7cfe add nix build
add nix build

chore: update cmakelists (wip)

chore: update cmakelists (wip)

working nix config

fix issues building app

fix path issue

working app

fix issue with paths
2025-10-28 15:41:52 -04:00
Iuri Matias 58c4b16c5a chore: move logos_app to its own repo 2025-10-08 13:38:08 -04:00