Repository navigation
Conversation
|
@claude Please review the current search-provider forwarding change, including registration teardown, plugin isolation and the wrapper coverage tests. Target is dev. |
|
@ninja-lifts Thank you for tracing the missing SearchProvider registration through both context wrappers and adding production-chain forwarding/teardown tests plus the Kotlin-reflection guard. I independently confirmed the actual consumer path: bookmarks registers its provider, DefaultPlugin forwards to SearchRegistryImpl, and GlobalSearchService reads that registry. This belongs in the host wrappers, with bookmark search behavior staying in the plugin. The four-file change integrates cleanly with dev 6004233 and passes diff whitespace checks. No new source correctness defect is established by this review. The process-wide same-provider-id teardown limitation is real and accurately disclosed: the production registry replaces/removes by id, and the two-plugin test uses distinct ids. Do not interpret that test as evidence of same-id multi-window ownership safety. A future ownership fix should test register A, replace with B under the same id, then tear down A without removing B; it is broader registry work and not a duplicate of #593 or #644. The current merge blocker is validation infrastructure: exact-head Build & Test run 34872616337 was action_required, with no check results, rather than failing source tests. I approved the fork workflow through the GitHub API. @claude was requested once in the preceding comment; no completed review exists yet. I am marking draft for this bounded validation handoff, not attributing the pending workflow/review to an author defect. Next steps: let that run complete, inspect any concrete failures, and verify PluginSearchProviderForwardingTest, PluginContextWrapperForwardingTest, the search suite and plugin-sandbox tests against fresh dev/API 1.0.93, with ktlint/detekt. Distinguish a Windows symlink-privilege setup failure from a regression using the baseline as you already did. Review useful Claude findings independently, then mark ready once required CI is green. A local Gradle rerun in this sweep did not acquire the shared build lock; your reported test evidence is not represented as my execution. No app was launched and no fixed-build manual bookmark search or multi-window UI check was performed. Merge only to dev under the shared lock with the reviewed head match. |
|
Claude finished @kshivang's task in 5m 26s —— View job Review: search-provider forwarding
Verdict: the fix is right, minimal and follows the established convention exactly. No blocking issues. Findings below are one documentation gap, two test-hardening suggestions and two nits — nothing that should hold the merge. What the change gets rightThe forwarding is byte-for-byte the sibling pattern. Capturing Teardown really is covered on every path, not just unload. Isolation is genuinely per plugin. The undo list is keyed by Nothing else in this repo needed the same edit. I grepped for every production Findings1. The multi-window consequence belongs in the code or AGENTS.md, not only the PR body. (documentation)
This is pre-existing behaviour for panels, MCP tool providers and status-bar items, and the PR's "Not covered" section states it accurately — I agree it is the right call, and owner-aware registration would be a change to the registries. The ask is only that the fact live next to the code: this repo's AGENTS.md already records the three-wrapper rule and names 2. The guard test misses members inherited from a
3. The guard names two classes literally. (test hardening, lower value) I confirmed those two are the only wrappers that exist, so this is not a gap today. Worth one line of KDoc saying a third Two things the guard deliberately cannot do, and the KDoc could say so in half a sentence: it matches by name, so an override that compiles but forwards to the wrong delegate, or drops the call, passes; and it will fail on any api version bump that adds a member. The second is the intended friction — the message names the missing members, which is the right failure — but a reader meeting it for the first time on an unrelated api bump will want the sentence. 4.
5. AGENTS.md requires On the test suite itselfThe four behavioural tests are well aimed: they build the real The honest gap the PR already names is that nothing asserts the end-to-end Verification limits on my sideI reviewed this statically. I could not compile or run the tests in this job: the Line lengths are within the 140-column |
|
Claude finished @kshivang's task in 5m 22s —— View job Review: search-provider forwarding through both plugin-context wrappers
VerdictThe diagnosis and the fix are correct, minimal, and land in the right place. I traced the whole path independently:
No correctness defect found. Comments below are nits and follow-ups. On the multi-window same-id limitationYour disclosure is accurate, and I can make the reachability concrete rather than hypothetical, because it is worth writing down for whoever picks up the follow-up:
That is strictly better than today (nothing registered in any window), and it is the same shape as Nits
Follow-up this PR makes live (not a change to request here)
What I could not verify
I have no blocking findings. Recommend merging once that run is green. |
11b6f26 to
6060a0e
Compare
|
Thanks @kshivang. Rebased onto dev From the reviews: the guard now includes inherited members and states what it cannot see, the listing comments and 593 + 150 tests (1 known Windows symlink-privilege failure), ktlint/detekt clean, the new mutations caught (4/4), 0 added conflicts across 57 PR/file pairs. |
6060a0e to
98e5485
Compare
A dynamic plugin's register() receives a TrackingPluginContext around a SandboxedPluginContext around the window's DefaultPlugin, and only DefaultPlugin put a SearchProvider into SearchRegistryImpl. Neither wrapper overrode registerSearchProvider or unregisterSearchProvider, so the call reached the PluginContext default, whose body does nothing. The bookmarks plugin registers its BookmarkSearchProvider this way, and GlobalSearchService gets bookmark results only from that registry. TrackingPluginContext now forwards both calls and records the teardown with recordUiExtensionRegistration, like panel menus, settings pages, deep-link handlers, shortcuts and status bar items, so unregisterAll removes the provider when the plugin is disabled or unloaded. SandboxedPluginContext forwards both calls like the registries above. PluginContextWrapperForwardingTest checks that both wrappers declare every PluginContext member. It uses Kotlin reflection because the api is compiled with -jvm-default=enable, which adds a compatibility bridge for each default method to implementing classes, so Java reflection reports members a wrapper never overrode.
… members Rebased onto dev, which moved to plugin API 1.0.93 (risa-labs-inc#693). That api adds PluginContext.organisationMembersProvider, which neither wrapper declared, so PluginContextWrapperForwardingTest failed on the rebased branch for both of them. Both wrappers now forward it. DefaultPlugin does not implement it yet, so every plugin still gets null; the forwarding only keeps a future host implementation from being swallowed by the wrappers. From review: - The guard now expects every member of PluginContext including inherited ones (less Any's), so splitting the api interface cannot quietly narrow it. A test with a small interface hierarchy pins that. Its KDoc states what it cannot see: members match by name, and a new wrapper has to be added to it. - The TrackingPluginContext comments that list the recorded kinds, and the unregisterAll KDoc, now name search providers. - AGENTS.md records search providers as the second three-wrapper gap. - PluginRegistrationsAcrossWindowsTest (from risa-labs-inc#694) left search out of its teardown-chain test because the wrappers did not forward it; it now covers all seven kinds.
98e5485 to
395753b
Compare
|
Rebased to clear the merge conflict. No content change: Same inherited-history cause as the earlier rebase on this branch. The #713 integration collapsed the individual PR commits, so the old base New head New exact-head CI is running; the prior head's checks do not validate this commit. |
|
@ninja-lifts The current six-file change and follow-up are reviewed. Both wrappers now forward search-provider registration/removal and organisationMembersProvider; tracked teardown uses the existing owner-aware host registry. The inherited-member guard and the updated two-window teardown test address the prior review, and #694 supplies the ownership behavior that the earlier handoff lacked. The name-only guard limitation is accurately documented; extra sandbox-local duplicate guards or unrelated logging cleanup are not required for this fix. Provider timeouts remain a separate search-service improvement, not a reason to duplicate or broaden these wrappers. I downloaded exact-head 395753b CI artifacts: all 13 focused forwarding/guard/two-window tests and 150 sandbox tests pass, with no failures or skips. All ten Build & Test checks pass, including Linux/macOS/Windows and quality (ktlint/detekt). A new local build was queued under the shared lock but stopped before Gradle began because the existing exact-head evidence is sufficient; no new local pass is claimed. Integration with refreshed dev is clean. No application was launched or visual bookmark search performed. Your code/tests/follow-up remain your contribution; this sweep made no source repairs. Prior original-work score 9/10 is preserved. Proceeding only with the locked exact-head squash merge into dev after one final refresh. |
* fix(plugins): let a plugin's search provider reach global search A dynamic plugin's register() receives a TrackingPluginContext around a SandboxedPluginContext around the window's DefaultPlugin, and only DefaultPlugin put a SearchProvider into SearchRegistryImpl. Neither wrapper overrode registerSearchProvider or unregisterSearchProvider, so the call reached the PluginContext default, whose body does nothing. The bookmarks plugin registers its BookmarkSearchProvider this way, and GlobalSearchService gets bookmark results only from that registry. TrackingPluginContext now forwards both calls and records the teardown with recordUiExtensionRegistration, like panel menus, settings pages, deep-link handlers, shortcuts and status bar items, so unregisterAll removes the provider when the plugin is disabled or unloaded. SandboxedPluginContext forwards both calls like the registries above. PluginContextWrapperForwardingTest checks that both wrappers declare every PluginContext member. It uses Kotlin reflection because the api is compiled with -jvm-default=enable, which adds a compatibility bridge for each default method to implementing classes, so Java reflection reports members a wrapper never overrode. * fix(plugins): forward organisationMembersProvider and check inherited members Rebased onto dev, which moved to plugin API 1.0.93 (#693). That api adds PluginContext.organisationMembersProvider, which neither wrapper declared, so PluginContextWrapperForwardingTest failed on the rebased branch for both of them. Both wrappers now forward it. DefaultPlugin does not implement it yet, so every plugin still gets null; the forwarding only keeps a future host implementation from being swallowed by the wrappers. From review: - The guard now expects every member of PluginContext including inherited ones (less Any's), so splitting the api interface cannot quietly narrow it. A test with a small interface hierarchy pins that. Its KDoc states what it cannot see: members match by name, and a new wrapper has to be added to it. - The TrackingPluginContext comments that list the recorded kinds, and the unregisterAll KDoc, now name search providers. - AGENTS.md records search providers as the second three-wrapper gap. - PluginRegistrationsAcrossWindowsTest (from #694) left search out of its teardown-chain test because the wrappers did not forward it; it now covers all seven kinds.
…ence (#876) * test(browser): run the form field detector against a fake DOM (#609) * test(browser): run the form field detector against a fake DOM FormFieldDetector is JavaScript living inside a Kotlin string, and it is the only one of those with no Kotlin test at all - so nothing read it, by grep or by execution, while it was injected into every frame of every page from BrowserHandleImpl.injectPageHelpers. This executes both of its scripts against a fake DOM covering only what they touch, so an added DOM read fails loudly. It follows test-browser-collector.js, which exists for the same reason and whose own note records that every budget bug in the collector so far was found by running it rather than reading it. It also closes a coupling the Kotlin side cannot see. The injected script emits nine keys and parseFieldInfoJson picks them back out with one hand-written regex per key, across two files and two languages. A rename on either side does not fail, it silently yields an empty string for that field forever. Both key sets are read from source here and compared. Four checks pin defects rather than desired behaviour. Each is marked and names its follow-up, and they are pinned rather than fixed so the fix lands as a reviewable diff against a measured baseline: - The script has no re-injection guard. A second injection into one document leaves two listener pairs, and the host re-runs injectPageHelpers on every main-frame NavigationFinished, which for a single-page app is a route change within one document. BrowserInteractionScript guards exactly this case, and the harness reads that file to confirm the omission is not house style. - Every focus is logged to the page's own console with the field's name or id. - The focusout handler dereferences document.activeElement unguarded. That is nullable per spec, and the throw leaves the blurred field still referenced. - parseFieldInfoJson matches "key":"([^"]*)" against JSON.stringify output, so a value containing a double quote is truncated and keeps a trailing backslash. The pattern is reconstructed from the Kotlin source rather than restated, so it cannot drift. Sources are read line-ending normalised, since a Windows checkout with core.autocrlf on has CRLF. * test(browser): model textarea type and normal focus transitions * test(browser): preserve independent timer failures and escaped values * test(browser): assert normal blur timers complete without errors --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(updater): reject overlapping host plugin updates (#612) * fix(updater): reject overlapping host plugin updates * docs(updater): clarify admission scope and shared progress notice --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(run): preserve concurrent configuration updates (#755) * fix(plugins): let a plugin's search provider reach global search (#670) * fix(plugins): let a plugin's search provider reach global search A dynamic plugin's register() receives a TrackingPluginContext around a SandboxedPluginContext around the window's DefaultPlugin, and only DefaultPlugin put a SearchProvider into SearchRegistryImpl. Neither wrapper overrode registerSearchProvider or unregisterSearchProvider, so the call reached the PluginContext default, whose body does nothing. The bookmarks plugin registers its BookmarkSearchProvider this way, and GlobalSearchService gets bookmark results only from that registry. TrackingPluginContext now forwards both calls and records the teardown with recordUiExtensionRegistration, like panel menus, settings pages, deep-link handlers, shortcuts and status bar items, so unregisterAll removes the provider when the plugin is disabled or unloaded. SandboxedPluginContext forwards both calls like the registries above. PluginContextWrapperForwardingTest checks that both wrappers declare every PluginContext member. It uses Kotlin reflection because the api is compiled with -jvm-default=enable, which adds a compatibility bridge for each default method to implementing classes, so Java reflection reports members a wrapper never overrode. * fix(plugins): forward organisationMembersProvider and check inherited members Rebased onto dev, which moved to plugin API 1.0.93 (#693). That api adds PluginContext.organisationMembersProvider, which neither wrapper declared, so PluginContextWrapperForwardingTest failed on the rebased branch for both of them. Both wrappers now forward it. DefaultPlugin does not implement it yet, so every plugin still gets null; the forwarding only keeps a future host implementation from being swallowed by the wrappers. From review: - The guard now expects every member of PluginContext including inherited ones (less Any's), so splitting the api interface cannot quietly narrow it. A test with a small interface hierarchy pins that. Its KDoc states what it cannot see: members match by name, and a new wrapper has to be added to it. - The TrackingPluginContext comments that list the recorded kinds, and the unregisterAll KDoc, now name search providers. - AGENTS.md records search providers as the second three-wrapper gap. - PluginRegistrationsAcrossWindowsTest (from #694) left search out of its teardown-chain test because the wrappers did not forward it; it now covers all seven kinds. * test(fluck): cover zoom and favicon preservation across tab copies (#777) * fix(fluck): preserve custom zoom level and favicon cache key across tab copy and split view * style: fix ktlint trailing blank line format in FluckTabInfoTest * fix(auth): read a pasted magic link the way the redirect function does (#740) "Paste magic link manually" rebuilt a boss://auth/verify link only when the pasted text contained a literal "token=". The sign-in email's link wraps the confirmation URL in the redirect function's url= parameter, and a copied link can carry it percent-encoded, so the paste was passed on as a web address, dropped as unrouted, and the screen showed nothing. Reproduced on BOSS 9.5.17 with a real email: the encoded link did nothing, and the same token with only url= decoded signed in. signInDeepLinkFor decodes the query and reads token or token_hash from the link or its url= value, with the type beside the token, else the outer link's, else magiclink, matching supabase/functions/redirect/app.ts. It refuses a token or type with characters that could add parameters to the rebuilt link, and a token on an address that is not a verify or redirect link. Text that is not a sign-in link now shows an error instead of nothing. * fix(tabs): keep tooltip visible while title updates (#752) * fix(home): add persistent navigation in the active pane (#757) * fix(home): offer direct Home entry from New Tab * feat(home): add persistent pane-local Home navigation * test(home): account for permanent Home in rail resize budget * fix(home): remove redundant Home creation from New Tab * fix(home): use subtle navigation selection instead of a filled strip * fix(home): highlight navigation content without full-row hover fill * fix(security): restrict plugin_ratings to the server-side rating pipeline (#761) plugin_ratings' four RLS policies (20260130000000) carry no TO clause, so they apply to anon and authenticated: - SELECT USING (true) let any holder of the shipped anon key read every rating row across all users - auth.users UUID, free-text review, and timestamps, unscoped by plugin visibility, the same cross-user leak class closed for plugin_downloads by 20260910120000 (#487). - The write policies only bind user_id to the caller, so a self-registered user can insert a rating for an arbitrary plugin UUID, using the FK violation as an existence oracle for org-scoped plugins. The desktop client never touches this table directly: the plugin-store Edge Function runs with the service role and routes/browse.ts reads aggregates through the SECURITY DEFINER stats functions, so restricting client access changes nothing for the shipped app. Mirror the downloads migration: REVOKE ALL from PUBLIC/anon/ authenticated, GRANT the four verbs to service_role, drop the permissive policies, and re-state the intent as explicit TO service_role policies. pgTAP coverage asserts no non-service-role policy remains (unscoped included), no per-verb client table privilege, and that service_role keeps the live path. * fix(ui): split the editor breadcrumb on path segments so it works on Windows (#753) The bottom bar built the breadcrumb by appending "/" to the project path, stripping that prefix and splitting on "/" alone. On Windows both the project path and the tab's file path are native (C:\Users\dev\repo), so the prefix never matched, the split found no separator, and the whole absolute path rendered as a single segment. Move the logic into editorBreadcrumbSegments, which treats both "/" and "\\" as separators - the display-label rule plugin-path-utils documents and extractFileName already follows - and compares whole segments, so a project at /repo does not claim /repo2/App.kt. The helper lives in its own file to keep BossBottomBar.kt under detekt's TooManyFunctions threshold. Adds EditorBreadcrumbSegmentsTest: 9 cases, none host-dependent. * fix(performance): read plugin process memory and threads per platform (#785) The Performance panel read memory and thread counts for out-of-process plugin JVMs with `ps -o pid=,rss=` and `ps -M` on every platform. `ps -M` is the macOS thread listing. Windows has no `ps` (Git for Windows' MSYS `ps` rejects both flags), so every plugin showed 0 bytes and 0 threads. On Linux, procps reads `-M` as a security-label column and prints one line per process, so every plugin showed exactly 1 thread. Move the query into PluginProcessMetrics: - Linux: VmRSS and Threads from /proc/<pid>/status, no subprocess. - macOS: the same two ps commands and counting rule as before. - Windows: Win32 through JNA, already a desktop dependency. Threads from a Toolhelp process snapshot, memory from GetProcessMemoryInfo. Not PowerShell: this runs every 5 s from the snapshot collector. Rewrite the ps -M counting loop as a pipeline with the same rule, and drop the two detekt baseline entries that named the deleted code. Adds PluginProcessMetricsTest: 10 cases, 9 of them host-independent. * fix(search): shift Spotlight file-name highlights by the platform separator (#716) searchFiles matches the query against IndexedFile.relativePath, then rebases the resulting ranges onto the file name, because FileResult's matchRanges index the name. It found the name's start with lastIndexOf('/'), but relativePath is derived from two absolute paths and so is joined with a backslash on Windows. The search returned -1, the ranges were shifted by nothing, and the dialog underlined whatever characters happened to sit at those offsets: `uiButton` against `app\ui\Button.kt` underlined "on" and "kt" rather than "Button", and a match landing past the name's length was dropped and underlined nothing. Look for the separator alongside '/', not instead of it. On a POSIX host the two are the same character, so that host is unchanged; on Windows '/' is not a legal file-name character and cannot split a name by accident. A bare lastIndexOf('\') on every platform would be wrong, since a backslash is a legal POSIX file-name character. ContentSearchService in this package already reaches for File.separatorChar for this reason. The separator is a parameter defaulting to the host's. Because the rule is deliberately platform-dependent, a Windows-shaped path proves nothing on a POSIX host - the shift there is correctly zero - so injecting it is what lets the Windows case be asserted on every runner rather than only on a Windows one. The four shift cases do that; the end-to-end case uses the host separator and is red only on Windows, which its KDoc states. * feat(plugins): add a copy button to plugin toasts (#745) * fix(run): make run-configuration disambiguation work on Windows (#653) * fix(run): make run-configuration disambiguation work on Windows makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath on a literal "/", but a stored filePath is an OS-native absolute path: DesktopMainFunctionDetector assigns file.absolutePath, which is backslash-separated on Windows. split("/") yields one part there, the parts.size >= 2 guard never passes, and two configurations with the same name in different directories stay byte-identical in the Run dropdown. Split on both separators, and take the project name from extractFileName() instead of substringAfterLast('/'). plugin-path-utils documents itself as the single source of truth for path utilities, and RunConfiguration.toShortNameWithProject already builds the initial label with it; this file was the one place re-deriving those values with a hardcoded separator. The project-name change also closes a latent leak: substringAfterLast('/') returns the whole absolute path on Windows, so the label would have read "main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed. Both functions become internal so the tests can drive them directly. * fix(run): keep the project bracket and drop empty path segments Addresses the review on 03ba543b. makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing on POSIX, but the branch never executed on Windows before this change, so the fix above is what makes it reachable there. loadSettingsSync assigns the rewritten names to _currentSettings and the next saveSettings persists them, so the loss is not display-only. The bracket is now carried across. Both splits drop empty segments, matching DesktopMainFunctionDetector .detectModuleName. A doubled separator can reach these functions through a hand-edited run-configurations.json, and without the filter takeLast(2) picks up the empty segment and labels it "/Main.kt". The filter subsumes the leading separator trim, which is removed rather than left as dead belt-and-braces. The project name is now read from projectPath.trimEnd('/', '\'), so a project path with a trailing separator still yields a bracket instead of dropping it. Both regexes move to object-level vals: the trailing-group pattern was duplicated across the two functions, and neither needs recompiling per element. Tests go from 10 to 16, adding bracketed stored names on both platforms, a doubled separator in each function, idempotency of the stored rewrite, and a projectPath that is not an exact prefix of filePath. * Gate JxBrowser auto-release on the compile-classpath modules (#678) * Gate JxBrowser auto-release on the compile-classpath modules The autorelease watcher's artifact-readiness probe only checked the core jxbrowser jar and the platform binaries. But the desktop compile classpath also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin, transitive of compose), each published as its own Maven module whose propagation can lag the binaries. When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules were not, so the gate passed, a build was dispatched, and every branding matrix job failed at desktopCompileClasspath with: Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1 Add the compile-classpath modules to the probe so a version is not dispatched until every artifact the branding build actually resolves is available. Fixes #667 * test: cover missing JxBrowser compile modules in release gate --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * feat(cli): report workspace health in boss status and add boss doctor (#579) * feat(cli): report workspace health in boss status and add boss doctor boss status --json gains an additive health object built from state BOSS already keeps: sandbox watchdog disables and plugin health rows from #454, a browser engine that is missing, failed or unresponsive, and MCP kill-switch and policy faults. boss doctor prints the same report, one line per finding with a suggested next step, and exits 2 while anything is reported. Nothing is changed by either command. doctor is registered in createBossCLI, the headless gate in CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor as a headless command. Refs #418 * fix(cli): keep health inspection read-only and qualify unchecked reports * fix(cli): contain each window's plugin health source and report partial coverage WorkspaceHealthSources.pluginSnapshots() mapped every registered window in one expression, so one source that threw propagated out and the collector's per-area try turned the whole plugins area into unchecked. A watchdog-stopped plugin in window A disappeared whenever window B's source failed, which is exactly the case the feature exists to report. Each window source is now contained on its own. Every snapshot that could be read is kept, failures are counted, and each failure is logged through BossLogger with LogCategory.SYSTEM the way the collector already logs an unreadable area. unchecked still means nothing could be read at all. A new additive partial set says an area was read from several sources and some of them failed, so the findings listed are real but do not cover the area. An area is never in both. boss doctor prints a "Partially checked:" line and stops saying "No problems found."; boss status appends "partially checked: plugins" to its Health line. Today only plugins can be partial, because it is the only area with one source per window. partial is additive on the wire: a reader that does not know the field sees what it saw before, and both CLI commands read an absent partial as empty, so an older CLI against a newer BOSS and the reverse both still parse. WorkspaceHealthSourcesTest now registers a failing source beside one that reports a stopped plugin and asserts through the real collector that the stopped plugin is still reported and the area is declared partial. Refs #418 --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(cli): write piped CLI output as UTF-8 (#604) Co-authored-by: Shivang <shivang.iitk@gmail.com> * feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468) * test(panels): retain sibling lifecycle isolation coverage from #313 Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work. * fix(panels): complete lifecycle cleanup across failure and window teardown Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions. * docs(ui): remove stale disabled top-bar feature list * test(ipc): protect advisory menu item ID uniqueness * fix(panels): keep store registration scoped to its window effect Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged. * fix(crash): address consolidated sanitization review notes Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate. * fix(logging): keep server failure payloads out of secret RPC logs Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately. * ci: build PR batches on dev * Allow Claude diff reviews after approved fork builds (#413) * fix(browser): preserve newer clipboard copies during plain-text paste (#316) * fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205) * fix(browser): make paste-without-formatting's restore identity-based, not text-based Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers. * fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount * fix(browser): blank lines between FakeTransferable overrides per ktlint * test(browser): retain clipboard restore scenarios from #408 * fix(browser): track clipboard ownership through AWT wrappers --------- Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com> * fix(plugins): serialize concurrent first loads per class name (#324) * fix(plugins): prevent duplicate class definitions during concurrent loads * fix(plugins): make class loading parallel-safe * test(plugins): pin concurrent loading lifecycle boundaries Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes. --------- Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com> Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412) * fix: make BrowserHandle JavaScript execution cancellable the previous synchronous JxBrowser call blocked the Main/EDT thread; coroutine cancellation could not take effect while that call was blocking; the implementation now uses JxBrowser’s asynchronous JavaScript callback API; suspendCancellableCoroutine makes the Kotlin waiting side cancellable; late callbacks after cancellation are safely ignored; this does NOT terminate JavaScript already executing inside Chromium. * fix: add robust native call lifecycle tracking for disposal * test: verify native operation cleanup on synchronous failure * fix: linearize browser disposal and pending operation tracking * fix: resolve detekt violations in browser native tracking * test: satisfy detekt failure simulation rule * fix: close native operation disposal race * fix(browser): drain an in-flight call before closing the browser Issue #300 reports that executeJavaScript is a synchronous, non- cancellation-aware native call, and a plugin's own withTimeoutOrNull around it can only abandon the caller's wait - not the call itself - which stays running on Main indefinitely and can race a concurrent handle.dispose(), touching the same native object from two threads at once. The severe half of this was already fixed by BoundedBrowserCall: this class confines every blocking round trip to one dedicated daemon thread instead of Dispatchers.Main, so a wedged renderer no longer freezes the app. What was not yet closed is the residual window this issue's core report is actually about: shutdown() stopped new work but did not wait for whatever was already running, so a caller could proceed straight to browser.close() while a call from just before shutdown was still finishing on the dedicated thread. shutdown() now waits, bounded, for that in-flight call to drain before returning. Costs nothing in the common case - awaitTermination returns immediately once the one worker thread and its queue are both idle, which is where an instance sits between calls - and only the bound in the rare case something was genuinely still running. Known, and disclosed in the KDoc rather than claimed away: this narrows the race for the common case (a call that was always going to finish quickly) rather than closing it. It cannot help when the in-flight call is itself the wedge this class's own deadline exists for - waiting unboundedly for a genuinely stuck call would reintroduce the exact freeze BoundedBrowserCall exists to prevent, just moved from a plugin's await into every caller's teardown path. There is no JxBrowser API on this version able to interrupt a blocking round trip already inside the native call, which is the whole reason the call is confined to its own thread instead of cancelled - closing that window completely needs JxBrowser's own cooperation, which the issue's own analysis already concluded is unavailable. Tests: shutdown waits for a fast in-flight call to finish before returning, and does not wait past its own drain timeout for a wedged one - the same before/after pair the rest of this test file already uses for BoundedBrowserCall's deadline. * fix(browser): defer native close until admitted calls drain Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring. * fix(browser): bound profile waits and clarify deferred cleanup ownership Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit. * fix(browser): schedule disposal after early UI teardown failure * ci: create launcher directory before Claude native installation * Revert "ci: create launcher directory before Claude native installation" This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0. * test: keep warmup registry ordering check on one event loop * test(browser): normalize CRLF in disposal source wiring guards --------- Co-authored-by: john k <johnk@johns-MacBook-Pro.local> Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com> Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com> * feat(tabs): show a speaker glyph on tabs that are playing audio (#314) * feat(tabs): show a speaker glyph on tabs that are playing audio (#308) * fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush 1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister. * fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures - BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire. * fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file * fix(tabs): publish audio state by browser ownership and keep icon layout stable --------- Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * feat: connect chrome density settings and keyboard controls (#305) * feat: wire the chrome density scale into settings and small-screen defaults ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on main and every bar (BossTitleBar, BossTopBar, the main tab bar, BossBottomBar) already read its height from BossChrome.dimens - but LocalChromeDimens was never provided anywhere, so it silently resolved to its staticCompositionLocalOf default (Comfortable) always. The scale was built and unreachable. This closes that gap: - WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so an existing install's settings file - which has never heard of this key - decodes to exactly the chrome it already had; no settings-version bump needed). - BossApp.kt now provides LocalChromeDimens from that field, host-only and outside BossAppCompositionLocals (plugins never draw host chrome). - A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious), indexed in Settings search. - A fresh install on a small screen (< 1000dp logical height, clearing the 13" MacBook Air's ~931-956pt from #239's own measurements) now defaults to Compact and starts with the bottom bar off - the one bar this manager can still reclaim itself, since the side strips are already off by class default. Toolkit.getScreenSize() is read once, wrapped in runCatching (HeadlessException off a display must not break a fresh install), with a pure defaultDensityFor(screenHeightDp) so the decision is unit-testable without a display. Addresses #239, scoped down from the full issue. Out of scope in this pass, deliberately: - The app does not yet report its own chrome budget as a measured percentage - the issue's own "state it, don't argue it" ask. Reachable later from the same ChromeDimens source, but needs a spot to render it. - The title bar's 27dp "Boss Console" label row is untouched - collapsing or merging it with the tab bar is a separate, riskier layout change than a density scale. - No regression intended to the existing per-bar show/hide switches or focus-mode edges: density only changes how much room a bar that is already on screen takes, never whether it is shown. * fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line Address review feedback on #305: - Removed showBottomBar = density != ChromeDensity.COMPACT from getDefaultSettings(). ChromeMetricsTest already shows the shipped macOS defaults reach 93.3% of a 931dp window at Comfortable, so #239's >=90% bar is met without touching the status bar - and that bar's own KDoc argues it must stay on ("the only always-on readout"). It was also reachable from the settings-load catch fallback, so a transient read error could silently drop the bar and have the next save persist the loss. Compact now only changes density, nothing else. - Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's default MaxLineLength (120), which applies here since .editorconfig's 140 is a ktlint-only setting and this line wasn't in the baseline. * feat: add chrome density setting * feat: add small-screen chrome defaults * feat: add chrome density controls and small-screen defaults * fix(ui): preserve lean density defaults and test density controls * style: format multiline screen-height lookup * fix(ui): separate density recovery and make editor chords opt-in * style: wrap density default documentation --------- Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(plugins): stop host resource fallback after unload (#325) * fix(plugins): stop host resource fallback after unload * fix(plugins): preserve resource warning for a missing result --------- Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * Handle closed browser frames during navigation callbacks (#400) * Handle closed browser frames during navigation callbacks * test(browser): retain closed-event guard after navigation integration * fix(browser): preserve helpers on PID failure and recognize closed transports * style(browser): wrap review diagnostics for detekt --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * refactor(window): pin the panel content alignment width (#398) * fix(window): restore strict width constraints for nested panels Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation. * Cover panel content alignment across width changes * Keep panel layout regression in the existing app test package * Document panel alignment contract and pin filling content width * Wrap the panel width assertion to satisfy detekt --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * #320 - Fix quick actions vanishing in short tab rail (#328) * #320 - Fix quick actions vanishing in short tab rail * Fix: detekt & ktlint checks * Fix rail action budgets and reversible fallback wiring * Fix resize test assertion import and update rail coverage note --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(crash): refine scroll boundary and bound error sanitization (#350) * fix(crash): refine scroll boundary and bound error sanitization * Avoid exposing a partial token at the crash message limit * Document sanitizer input bound and verify expansion ordering --------- Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(logging): redact private hostnames (#346) * fix(logging): redact private hostnames * Handle private hostname punctuation and preserve diagnostic ports * Format hostname punctuation regression assertion --------- Co-authored-by: ayush <workside@gamil.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * feat: Opening an .html file should ask open as a file or webpage (#333) * feat: Opening an .html file should ask open as a file or webpage * Fix HTML prompt delivery and persisted preference ordering * Cover HTML routing and correct review test fixtures * Honor HTML navigation intent and release pending prompts on failure * Isolate HTML prompt handling from the general dialog host --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(plugin-store): parse timestamps consistently in list and detail metadata (#379) * Fix: Implement proper ISO timestamp parsing in PluginStoreClient * fix(repository): implement robust ISO timestamp parsing for Supabase payloads * style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught * fix(plugin-store): implement plugin metadata timestamp parsing PluginStoreClient.parseTimestamp was a stub that always returned 0L, so every plugin fetched from the store showed "Last Updated"/"Published" as the Unix epoch in the Toolbox UI regardless of what the server actually sent (#337). Parses the ISO-8601 timestamp (the shape the store sends, e.g. 2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant, falling back to 0L for a blank, missing, or malformed string rather than throwing - matching the field's own empty-string default for a response that omits it. java.time rather than adding kotlinx-datetime as a new dependency: this module already uses java.util.* directly in the same commonMain source set (it has one real target, jvm("desktop")), so nothing is gained by introducing a second time library for one function. Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the convention already established next to it in PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset timestamp, a blank string, and a malformed string. * Fix timestamp normalization edge cases and verify both response mappings * Use release publication dates and pin timezone-independent fallback --------- Co-authored-by: Sanjay <sanjaysaini4423@gmail.com> Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * feat(remote-ui): authenticate and expose remote UI surfaces (#348) * feat(remote-ui): authenticate remote UI process identity * feat(remote-ui): place authenticated remote UI surfaces * fix(remote-ui): close ownership and placement lifecycle races * test(remote-ui): expose placement recovery cases to JUnit --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * feat(dashboard): add shared read-only What's New release feed (#381) * Add What's New release feed to dashboard * Serialize update settings persistence * Remove unused update settings import * fix(updater): retain realtime refreshes during shared release fetches * style(updater): format shared release fetch regression tests * fix(updater): coalesce release refresh bursts and preserve visible history --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(language): consolidate host and out-of-process language-id tables (#358) * fix(language): consolidate host and out-of-process language-id tables EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor) each hand-maintained their own extension-to-language-id table because boss-app-editor - a plain JVM module compiled to a GraalVM native image - could not depend on anything in composeApp. They disagreed: EditorServiceImpl named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and EditorServiceImpl was missing more than forty ids the other table had (fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75). Moves the table into a new plugin-platform/plugin-language-types module - dependency-free by design, so it carries nothing extra into boss-app-editor's native-image build - and has both EditorLanguages and EditorServiceImpl read from it. This is the first time anything under modules/ depends on plugin-platform/; verified compiling and passing tests before committing to the approach. EditorLanguages keeps its existing public API unchanged (same values for every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry and "plaintext" fallback as local additions on top of the shared table, rather than folding them into it: proto isn't part of the boss-file-types.json default-app-association surface the shared table backs, and "plaintext" is this service's own gRPC default, not a value composeApp reads. Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously no tests existed for this class at all) - the latter pins the actual bug fix, that detectLanguage("sh") now returns "bash" instead of "shell". Out of scope, and disclosed rather than silently dropped: two of the five duplicate tables the issue names - the editor-tab plugin's own LanguageDetection and BossEditor's lexer registry - live in separate repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in plugin-icons is also not consolidated: it keys icon selection off the raw filename to preserve distinctions a language id can't carry (package.json, yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different brand icons) and never computes a language id in the first place, so routing it through this table would need a separate icon-selection redesign. * fix(language): keep file-association drift checks on the shared table * fix(language): apply shared filename rules to editor file opens --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * Stop anon reaching schema public by inheritance, and scope identity to real orgs The BOSS Supabase anon key is compiled into this repo, which is public. Anything `anon` can execute is therefore executable by anyone. Verified on 2026-09-08: * get_encryption_key() returned the Vault master encryption key, unauthenticated. decrypt_text() was anon-callable beside it, making a decryption oracle over everything encrypt_text protects - user secrets and the QBO/Google broker credentials. * find_user_by_email() confirmed any address and returned its uuid. * list_shareable_recipients() returned 152 users WITH full email addresses to any self-registered account, and 82 @risalabs.ai addresses on a search. * arcade_leaderboard() and arcade_bs_standings() published the roster of everyone who had opened the Arcade, unauthenticated. None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on every new function, PUBLIC includes anon, and this project's default privileges add anon on top - so a SECURITY DEFINER function is internet-callable from the moment it is created. Both revokes are traps in mirror image: `from public` leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses the entry and new functions still get `=X`). 20260908000000 event trigger: revoke PUBLIC+anon on every function created in schema public. An explicit `grant ... to anon` after the create still wins - anon access must be stated, never inherited. 20260908010000 org_visible_users / org_is_vetted / user_display_name: ONE definition of "may this account learn who that account is", shared by the Arcade and poker rather than copied. Vetted means a human approved the join, which excludes the catch-all `boss` org every account joins on signup (153 members, 20 domains). 20260908020000 route list_shareable_recipients through both. It was already org-scoped, which is why it looked right; it accepted ANY shared org. 20260908030000 one-time sweep of the ~30 functions that already had the inherited grant. The RBAC mutators fail closed (is_user_admin(NULL) is false), so this is defence in depth, not a patched escalation. The rule is a SET, not a per-target predicate: as a per-row qual it is pushed below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score rows for 45 players, against 37ms. supabase/audit/identity_disclosure_audit.sql is the standing check, because "we fixed the leak" is not a durable claim. All four checks report HEALTHY. Left deliberately anon-callable, each documented where it is kept: the plugin store browse API, the three helpers called from RLS policies on anon-readable tables, and custom_access_token_hook. Verified after the sweep that anonymous plugin-store browse and signed-in RBAC reads both still work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(editor): contain write-local stack overflow and preserve fatal errors (#396) * Make writeFileContentSafe actually safe: catch the Errors it was named for `writeFileContentSafe` exists to return false rather than throw. It did not hold that contract: } catch (e: Exception) { fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e) false } StackOverflowError is an Error, not an Exception, so it went straight through - out of writeFileContent, out of the editor_write_file MCP handler, and to the caller as a bare StackOverflowError instead of the handler's own "Write failed for <path>". The warn that would have named the file never ran either. That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two independent reports of it, on unrelated content, and #27's complaint is precisely the missing diagnosis: the error "gives no indication of which input caused it". StackOverflowError and OutOfMemoryError are caught explicitly rather than widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and readFileContentSafe in this same file already treats OutOfMemoryError as a reportable outcome. Throwable would also absorb LinkageError and ThreadDeath, which are not this function's to swallow - there is a test asserting a NoClassDefFoundError still propagates. The failure log now names the path and the character count. Not the content: these writes carry whatever the user is editing. **This does not fix the write.** Whatever recurses is upstream of this function, which is mkdirs plus writeText - I ruled out the plugin's handler, this write, and the host's MCP argument parsing, and said so on the issue. What changes is that the failure is reported honestly instead of escaping, which is where a diagnosis can start. The body moves to `guardedWrite` with an injectable write, because a test JVM cannot be made to overflow the stack inside writeText on demand. Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt. Refs risa-labs-inc/boss-plugin-editor-tab#18, #27. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(editor): clarify write guard scope and prior recursion fix * fix(editor): preserve fatal heap errors in write guard --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(state): apply version-checked JSON merge patches (#361) * Fix: Implement proper ISO timestamp parsing in PluginStoreClient * Feature: Implement JSON Merge Patch for plugin delta state synchronization * fix(state): validate delta bases and exclude duplicate timestamp scope * fix(state): reject malformed and deeply nested patches --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363) Bumps `grpc` from 1.83.1 to 1.84.0. Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0) Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0) Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0) Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0) Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0 - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-netty dependency-version: 1.84.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.grpc:grpc-protobuf dependency-version: 1.84.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.grpc:grpc-stub dependency-version: 1.84.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.grpc:grpc-services dependency-version: 1.84.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: io.grpc:grpc-util dependency-version: 1.84.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364) Bumps `protobuf` from 4.36.0 to 4.36.1. Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1 Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1 - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Commits](https://github.com/protocolbuffers/protobuf/commits) --- updated-dependencies: - dependency-name: com.google.protobuf:protobuf-kotlin dependency-version: 4.36.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.google.protobuf:protobuf-java dependency-version: 4.36.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * Add the master-key rotation procedure, and rotate get_encryption_key() was anon-callable, so the Vault master_encryption_key was retrievable by anyone holding the project anon key - which ships compiled into this public repo. Revoking access does not un-disclose a key, so it was rotated on 2026-09-09 and the 184 rows encrypted under it re-encrypted. Scope of the original exposure, stated precisely because the capability was worse than the reach: decrypt_text() was anon-callable beside it, so an attacker holding ciphertext from ANY channel - a backup, an export, a screenshot - could decrypt it through the API without even needing the key. But no bulk read path to that ciphertext existed: qbo_token_state and google_token_state have no client grants at all, and all 178 secrets rows have org_id NULL, so the secrets policy reduces to owner-only. This was a serious latent vulnerability - one org-shared secret, one RLS slip, or anyone with backup access would have had plaintext - rather than a confirmed bulk disclosure. The script is kept because rotation is not a one-off. It is atomic (one DO block), self-verifying (md5 of every row's plaintext captured before, re-derived after through decrypt_text so the check covers both the swap and the data), and a mismatch or row-count drift raises and rolls back, so a broken rotation cannot commit. Verified after the run: the live key differs from the disclosed one, 184/184 rows decrypt under it, and the disclosed key opens 0 rows. The old key is retained in the Vault under an explicit name. Do NOT delete it while pre-rotation backups exist - they are encrypted under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(plugins): defer stale jar cleanup until restart (#340) * fix(plugins): defer stale jar cleanup until restart * fix(plugins): satisfy quality checks * Preserve staged plugin artifacts when another plugin updates * Require cleanup intent and retain artifacts with unordered manifest versions --------- Co-authored-by: ayush <workside@gamil.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(plugins): defer fluck-browser hot-reload to a restart (#352) * fix(plugins): defer stale jar cleanup until restart * fix(plugins): defer fluck-browser hot-reload to a restart Hot-reloading fluck-browser force-unloads the classloader that created its JxBrowser native views. Every open tab - and every tab opened afterward, since the factory that would recreate the view is gone too - draws an empty box, and a second hot-reload does not recover it; only a full restart does. HotReloadPolicy names plugins that must never be swapped in place, matching the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at every path that can force-unload a live plugin to apply an update: the resetPluginInstances/doReloadPlugin path that actually reproduced this bug, the menu-driven Reload action, the Toolbox update flow, and the store installer. For a not-hot-reloadable plugin, the new jar is staged into installed.json without touching the running instance or its open tabs, and a status message tells the user to restart to apply it - the next cold start loads the update with a fresh classloader. Also fixes two call sites that asked "restart dependent plugins?" before checking whether an unload was even going to happen, which produced a confusing prompt for an unload that a deferred update was never going to do. * fix(plugins): satisfy quality checks * Preserve staged plugin artifacts when another plugin updates * Validate deferred browser artifacts and reject ineffective downgrades * Require cleanup intent and retain artifacts with unordered manifest versions * Preserve unloaded-browser recovery and unify safe deferred selection * Format scoped update cleanup integration --------- Co-authored-by: ayush <workside@gamil.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365) * Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest. * Serialize plugin spawn registration with reaping and preserve replacement handles * Fence late process creation without blocking shutdown and cover recovery --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix(plugins): complete loader cleanup after disposal linkage errors (#302) * fix: API hot swap during startup * fix: workflows * resolved comments by maintainer * fix CI and build * Scope disposal error resilience and cover loader cleanup after linkage failure --------- Co-authored-by: Shivang <shivang.iitk@gmail.com> * Fix shortcut rebinding key storage (#329) (#335) * #329 - Fix: store key names when rebinding shortcuts * Fold the key spellings Compose renders but the AWT interceptor does not BOSS maintains two key-name vocabularies over one keyboard. `AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's `Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both get a name, names it another. Neither list is derived from the other, and where they disagree a chord fires on one path and silently does nothing on the other. They had drifted on fourteen keys. Nine are reachable in a running app, because `Key.toString()` falls through to AWT's `getKeyText`, which answers with the macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home, End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was "Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab (TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`, `Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded for the same reason. This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so the fix is entries in the table those live in rather than a new mechanism. Also here, because the divergence is what they are about: - `composeKeyName` is the one place that reads a name out of `Key.toString()`. `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is how one path came to know a spelling the other did not. - `KeyVocabularyAgreementTest` walks the two tables key by key over all 73 bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is hand-written, since that is the identity of the key itself; both vocabularies are read from the code under test, so the next divergence fails a build rather than a keystroke. A second test asserts the fold does not collapse keys that are distinct, which asserting agreement alone would pass without. - `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the agreement test's input is whatever the environment renders and a cold run cannot reach the glyphs. `getKeyName` becomes `internal` so the test can read it; the two detekt baseline entries are rekeyed for the visibility change, with the function unchanged. Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt. Mutation-checked - reverting the new aliases fails the agreement test and names every key it lost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Store a folded key name on capture, and repair the keymaps that hold a keyCode Completes #329, on top of the one-line capture fix in #335. which is the right shape and stops new rebinds being dead. Its own description records the rest as future work: the keymaps already on disk still hold numeric keys, and nothing tests it. This is that rest. **Fold the name before storing it.** The raw rendering is not the presets' vocabulary and it is not stable. The left arrow renders "Left" against the presets' "DirectionLeft", the right bracket "Close Bracket" against "CloseBracket", the 1 key "1" against "One" - all of which MATCH through the alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows "directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT" beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`, which answers with a word while the toolkit is cold and the macOS glyph once it is up, so the same user rebinding Tab gets "Tab" or the glyph depending on nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which makes a rebind indistinguishable from a preset binding: same match, same signature, same rendering, and deterministic. **Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves a stored keyCode so an unmigrated file keeps matching, AND the settings migration rewrites it so the file stops holding a ten-digit key. Either alone is not enough: the migration cleans one file, and a keymap restored from a backup, copied off another machine, or exported and re-imported reaches the matchers before it reaches the migration. **The repair runs before `migrateSettings`' chord arithmetic**, which is what lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as a numeric key, `chordHolders` produced a signature no preset chord could equal, so a keymap whose owner had rebound an action through the UI read as not claiming that chord and the guard handed a new action straight onto it. #322's own motivating example is "someone who rebound panel.navigate_right", which is a UI rebind: the guard was protecting hand-edited files and not the ones made in the app. Two smaller things: - The dialog's preview was rendering the raw keyCode too. It had a private copy of the display formatter, and now renders the exact `KeyStroke` that Apply persists, so the preview cannot disagree with what is saved. - `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has no caller today, which is exactly why it would have outlived the fix. Answering the question the issue leaves open: yes, the Shortcuts screen's own tester reports these, so it reproduces without a build change. `ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and the row goes red. Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Preserve MoveHome and MoveEnd aliases from PR #405 Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary. * Keep unrecognised legacy key codes intact during repair * Exercise warm toolkit key names and correct rendering comments * Keep shortcut capture consumers consistent and reject modifier-only captures * Keep import regression setup readable * Order warm-toolkit test imports per repository style * Recognise unknown-key diagnostics independently of their localized prefix * Wrap modifier key lists for quality gates * Share the dispatcher modifier and lock-key exclusions with capture * Use public shortcut equality in the formatted menu regression test * Preserve malformed native codes instead of storing invalid Unicode * Cover the new dev chrome-density shortcut in capture round trips --------- Co-authored-by: Arjun Singla <singlaarjun28@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix: make sidecar backfill lifecycle reliable (#347) * fix: make sidecar backfill lifecycle reliable * test(plugins): format sidecar backfill tests * fix(plugins): keep backfill persistence on IO and cover lifecycle transitions * fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO * style(plugins): format sidecar coordinator declarations * style(plugins): keep unsigned predicate within line limit * style(plugins): wrap unsigned predicate expression * style(plugins): name the current stamp in unsigned predicate --------- Co-authored-by: ayush <workside@gamil.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * fix: await SupabaseConfig initialization before syncing plugin manifest (#366) * fix: await SupabaseConfig initialization before syncing plugin manifest * style: fix ktlint import order for java.io.File * fix(plugin): wait for SupabaseConfig before starting the manifest sync SystemPluginManifestService.startSync() ran its startup fetch and Realtime subscription before SupabaseConfig finished its own async initialize() call in the Compose UI layer, so both routines' first attempt hit SupabaseConfig.client's "not initialized" throw: a startup warning for the fetch, and an unnecessary first trip through the subscription's backoff retry loop before it ever connects (#370). SupabaseConfig.isInitialized already exists as a StateFlow for exactly this. Both of startSync's launched coroutines now suspend on it once, before touching the client, replacing a guaranteed-to-fail-once startup path with a wait for the real precondition. The existing "catch up on (re)connect" comment in subscribeToChanges - added because this same race could also just lose - stays accurate; this closes the race rather than only compensating for it afterward. Test exercises the actual mechanism: a coroutine awaiting SupabaseConfig.isInitialized genuinely suspends (verified via TestCoroutineScheduler.runCurrent(), not a coroutine that was merely scheduled and never run) until initialize() is called, then resolves. * Isolate manifest readiness regressions from stored auth sessions * Test manifest readiness without singleton side effects --------- Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com> Co-authored-by: Shivang <shivang.iitk@gmail.com> * Fix heavyweight modals dismissing destructively on focus loss (#152) (#353) * Fix heavyweight modals dismissing destructively on focus loss (#152) Extending LocalHeavyweightOverlays to the whole BossWindow content routed several dialogs through HeavyweightModal, which dismisses whenever focus leaves the application (oppositeWindow == null). Two dialogs dismiss to a destructive action, so this was a regression: - MemoryPressureNoti…
Summary
A plugin's
SearchProvidernever reached global search.PluginContext.registerSearchProvideris the documented way for a plugin to contribute to "the application's global search (Spotlight)" (its KDoc in boss-plugin-api, andPLUGIN_DEVELOPMENT.mdin boss-plugins), and the bundled bookmarks plugin calls it inregister(). The call was dropped before reaching the host.Reproduced in the released app (BOSS v9.5.17, Windows 11): with the bookmarks plugin loaded, a tab bookmarked into Favorites ("Page1 - Unified Sales Platform | Email, WhatsApp, SMS & More") shows in the Favorites panel, but after closing the tab, global search for
Unified Saleslists only a Recent Pages result and no Bookmarks section.Root cause
DynamicPluginManagergivesregister()aTrackingPluginContextaround aSandboxedPluginContextaround the window'sDefaultPlugin(all fiveregister(trackingContext)call sites, including the out-of-process "UI in-process" branch).DefaultPluginimplementsregisterSearchProvider(intoSearchRegistryImpl). Neither wrapper overrode it orunregisterSearchProvider, and both are plain: PluginContextclasses, so the call reached the interface default, whose body is empty.GlobalSearchServicegets bookmark results only fromSearchRegistryImpl.providers, so the bookmarks plugin'sBookmarkSearchProvidercontributed nothing.SandboxedPluginContextalready states the rule above its UI extension overrides ("Without these overrides the PluginContext interface defaults (no-ops) would swallow plugin registrations silently"); search providers were not in that block. AGENTS.md records the same class of gap formcpToolRegistry.Fix
TrackingPluginContext: forward both calls, and record the teardown withrecordUiExtensionRegistration, exactly like panel menus, settings pages, deep-link handlers, shortcuts and status bar items, sounregisterAllremoves the provider when the plugin is disabled or unloaded.SandboxedPluginContext: forward both calls, like the registries above them.organisationMembersProvider, which plugin API 1.0.93 (ondevsince fix(browser): integrate exact-frame editor routing with API 1.0.93 #693) added toPluginContext.DefaultPlugindoes not implement it, so every plugin still getsnull; the forwarding keeps a future host implementation from being swallowed the same way. Without it, the guard below fails on currentdevfor both wrappers.No change to
DefaultPlugin,SearchRegistryImplorGlobalSearchService.Guard
PluginContextWrapperForwardingTestchecks that both wrappers declare everyPluginContextmember, so the next member added to the api cannot fall through to a silent default in one wrapper.HomeActionRoutingTestdoes): the api is compiled with the default-jvm-default=enable, which adds a compatibility bridge for each default method to implementing classes, soClass.declaredMethodslists members a wrapper never overrode.PluginContextincluding inherited ones (lessAny's), so splitting the api interface into a base and extensions cannot quietly narrow it. A third test pins that with a two-level interface.organisationMembersProviderforwarding removed, it reports exactly[organisationMembersProvider]for each wrapper, and the original declared-members version of the check reports the same.Verified
On
devd22d2bfeb(plugin API 1.0.93, #694 merged):Red first (original submission, on
dev5a2e8181): all 6 new tests failed.PluginSearchProviderForwardingTest(4) builds the production chain,TrackingPluginContext(SandboxedPluginContext(InProcessPluginSandbox, host)), withDefaultPluginreplaced by a recording host: register and unregister reach the host,unregisterAllremoves the provider, and one plugin's teardown leaves another plugin's provider registered.With fix(plugins): keep a plugin's app-wide registrations when another window closes #694 merged, the multi-window case is now covered end to end.
PluginRegistrationsAcrossWindowsTestleft search out of its teardown-chain test because the wrappers did not forward it ("fix(plugins): let a plugin's search provider reach global search #670"); it now covers all seven kinds, so a closed window's plugin teardown is shown to keep the other window's search provider through the real wrappers and two realDefaultPlugins.:composeApp:desktopTestforai.rever.boss.components.plugin.*andai.rever.boss.search.*: 593 tests, 1 failure,DesktopFileScannerTest"follows a symlink" withFileSystemException: A required privilege is not held by the client(Windows without Developer Mode cannot create symlinks; unrelated, fails the same way without this change).:plugin-platform:plugin-sandbox:desktopTest: 150, 0 failures.ktlintCheckanddetektclean forcomposeAppandplugin-sandbox.Collision check: a 3-way merge of the six changed files against every open PR that changes them (57 PR/file pairs) adds 0 conflicts.
Mutation-checked, each caught:
mcpToolRegistry)organisationMembersProviderPluginContextdeclares itselfNot covered
GlobalSearchServiceisolates a provider that throws, but has no per-provider timeout and joins all sources withawaitAll. That was unreachable while the registry was always empty; this change makes plugin code run there for the first time. It is aGlobalSearchServicequestion, not a wrapper one.printlncalls inTrackingPluginContext.unregisterAllare left alone.Update after review (2026-09-15)
devd22d2bfeb; the original commit applied with no conflicts.unregisterAllKDoc, name search providers; AGENTS.md records this as the second three-wrapper gap. The earlier multi-window note is resolved by fix(plugins): keep a plugin's app-wide registrations when another window closes #694, and its test now includes search.organisationMembersProviderforwarding (above).plugin-sandbox(PR CI runsbuild, which already runs this test).