fix(settings): wait for the loader before migrating the retired settings
The one-time import of a pre-0.1.7 `settings.yaml` section never ran. It
started from the `ctx.inject(['settings'], …)` callback, which fires the
moment the settings SERVICE appears — while the loader is still mounting
rows, so `describe()` does not list this plugin's own entry yet, the lookup
returns undefined, and the import bailed on "no form" without a trace.
Upstream's own migration waits (`ctx.root.loader.await()`); this does the
same.
That made every early return of the import invisible, which is how it went
unnoticed: the callback simply produced nothing. It now returns a named
outcome ('imported' / 'no-profile-home' / 'no-form' / 'already-configured' /
'no-legacy-section' / 'rejected') and the caller logs it — anomalies at warn,
the rest at info. The `loader` mirror's `await` is optional so a composition
without the cordis loader still reaches the import instead of aborting the
whole settings face inside the inject callback; the smoke fakes model it.
Verified on the live 3080 instance: after the upgrade the settings form
reports the user's real values back (autoOpenJobs false, workspaceFence
false, the nine-entry tabsEnabled map), where before it reported pure
defaults. The mount lane stays 7/7.
This commit is contained in:
@@ -495,6 +495,11 @@ export interface SidebarSettingsService {
|
||||
*/
|
||||
export interface SidebarLoaderService {
|
||||
entries(): Iterable<SidebarLoaderEntry>
|
||||
/**
|
||||
* Resolves once every profile entry has been mounted (the Loader settles).
|
||||
* Optional: a composition without the loader imports immediately instead.
|
||||
*/
|
||||
await?(): Promise<unknown>
|
||||
}
|
||||
|
||||
/** One configured plugin row inside the profile's entry tree. */
|
||||
|
||||
+47
-9
@@ -622,6 +622,28 @@ async function readLegacyPrefs(home: string): Promise<Record<string, unknown> |
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a legacy preference import did or did not happen.
|
||||
*
|
||||
* Every one of these is a normal outcome on some deployment, but a silent
|
||||
* early return is exactly the failure shape this release is full of: an
|
||||
* operator upgrading a host cannot tell "there was nothing to migrate" from
|
||||
* "the import never ran". The caller therefore logs the outcome.
|
||||
*/
|
||||
type LegacyImportOutcome =
|
||||
/** The retired section was found and written into the row. */
|
||||
| 'imported'
|
||||
/** No `profileContext`, so the home (and the document) cannot be located. */
|
||||
| 'no-profile-home'
|
||||
/** The settings service exposes no form for this row. */
|
||||
| 'no-form'
|
||||
/** The row already carries user values; the import must never overwrite them. */
|
||||
| 'already-configured'
|
||||
/** Neither the live nor the migrated document carries a usable section. */
|
||||
| 'no-legacy-section'
|
||||
/** The write itself failed; the caller's catch reports it. */
|
||||
| 'rejected'
|
||||
|
||||
/**
|
||||
* One-time import of the Side card preferences a pre-0.1.7 release persisted.
|
||||
*
|
||||
@@ -638,22 +660,23 @@ async function readLegacyPrefs(home: string): Promise<Record<string, unknown> |
|
||||
* @param ctx - host plugin context (profile home, logger).
|
||||
* @param settings - the settings forms service.
|
||||
* @param ns - this plugin row's entry id.
|
||||
* @returns which outcome the import reached.
|
||||
*/
|
||||
async function importLegacyPrefs(
|
||||
ctx: Context,
|
||||
settings: SidebarSettingsService,
|
||||
ns: string,
|
||||
): Promise<void> {
|
||||
): Promise<LegacyImportOutcome> {
|
||||
const home = ctx.profileContext?.home
|
||||
if (home === undefined) return
|
||||
if (home === undefined) return 'no-profile-home'
|
||||
const row = settings.describe().find(candidate => candidate.ns === ns)
|
||||
if (row === undefined) return
|
||||
if (row === undefined) return 'no-form'
|
||||
const user = row.user
|
||||
if (user !== null && typeof user === 'object' && Object.keys(user).length > 0) return
|
||||
if (user !== null && typeof user === 'object' && Object.keys(user).length > 0) return 'already-configured'
|
||||
const section = await readLegacyPrefs(home)
|
||||
if (section === undefined) return
|
||||
if (section === undefined) return 'no-legacy-section'
|
||||
await settings.update(ns, section)
|
||||
ctx.logger.info('dsh-better-sidebar: imported Side card preferences from %s', LEGACY_SETTINGS_FILE)
|
||||
return 'imported'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -764,9 +787,24 @@ export function apply(ctx: Context, config?: SidebarConfig): void {
|
||||
syncOpenToolsGate()
|
||||
// A pre-0.1.7 release persisted these preferences through the file-backed
|
||||
// settings provider, which this release deleted. Import that section once.
|
||||
void importLegacyPrefs(ctx, sctx.settings, ns).catch((error: unknown) => {
|
||||
ctx.logger?.warn?.('dsh-better-sidebar: legacy preference import skipped')
|
||||
ctx.logger?.warn?.(error)
|
||||
//
|
||||
// The loader must settle first: `describe()` only lists an entry whose
|
||||
// fiber is ACTIVE, and this callback runs the moment the settings SERVICE
|
||||
// appears — while the loader is still mounting rows, so this plugin's own
|
||||
// row is not in the form list yet and the import would silently find
|
||||
// "no form" and do nothing. Upstream's own settings migration waits the
|
||||
// same way (`ctx.root.loader.await().then(...)`).
|
||||
void Promise.resolve(ctx.loader?.await?.()).then(
|
||||
() => importLegacyPrefs(ctx, sctx.settings, ns),
|
||||
).then((outcome) => {
|
||||
if (outcome === 'no-profile-home' || outcome === 'no-form') {
|
||||
ctx.logger.warn('dsh-better-sidebar: legacy preference import could not run (%s)', outcome)
|
||||
return
|
||||
}
|
||||
ctx.logger.info('dsh-better-sidebar: legacy preference import: %s', outcome)
|
||||
}).catch((error: unknown) => {
|
||||
ctx.logger.warn('dsh-better-sidebar: legacy preference import was rejected')
|
||||
ctx.logger.warn(error)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -727,6 +727,9 @@ const mountWithSettings = (settings?: unknown, home?: string): SidebarWebRoute =
|
||||
// fake without these entries has NO settings face at all.
|
||||
loader: {
|
||||
entries: () => [{ options: { id: ENTRY_ID, name: 'dsh-better-sidebar' }, fiber: PLUGIN_FIBER }],
|
||||
// The real loader settles before a form is addressable; the fake
|
||||
// resolves immediately so the import runs on the same tick.
|
||||
await: () => Promise.resolve(),
|
||||
},
|
||||
logger: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
|
||||
...(home === undefined ? {} : { profileContext: { home } }),
|
||||
@@ -930,6 +933,9 @@ describe('agent sidebar-open tool gating', () => {
|
||||
fiber: PLUGIN_FIBER,
|
||||
loader: {
|
||||
entries: () => [{ options: { id: ENTRY_ID, name: 'dsh-better-sidebar' }, fiber: PLUGIN_FIBER }],
|
||||
// The real loader settles before a form is addressable; the fake
|
||||
// resolves immediately so the import runs on the same tick.
|
||||
await: () => Promise.resolve(),
|
||||
},
|
||||
logger: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user