···4545 clearAllTokens,
4646} from './tile-tokens.js';
4747import { scopes, publish, getSystemAddress, unsubscribeAllByPrefix } from './pubsub.js';
4848-import { DEBUG } from './config.js';
4848+import { DEBUG, getTilePreloadPath } from './config.js';
4949import { loadSchemaDefaults } from './tile-settings-defaults.js';
5050+import { getExtensionPath } from './protocol.js';
50515152// Short grace period given to tiles to run their `api.onShutdown()` callbacks
5253// before the BrowserWindow is forcibly closed. Kept small so app-quit and
···609610 revokeTokensForTile(tileId, entryId);
610611611612 DEBUG && console.log(`[tile-launcher] Unloaded tile: ${key}`);
613613+}
614614+615615+/**
616616+ * Relaunch a tile — re-read the manifest from disk, close the old window
617617+ * (if any), revoke its tokens, then call `launchTile()` with the fresh manifest.
618618+ *
619619+ * This is the v2 replacement for `createExtensionWindow(extId)` used by
620620+ * `reloadExtension()` and `loadDevExtension()` in main.ts. See Phase 2.5 #3b
621621+ * in `docs/v1-removal-plan.md`.
622622+ *
623623+ * Edge cases:
624624+ * - Tile not currently running (no window): just launches fresh.
625625+ * - Window already destroyed: skips close, still revokes tokens and relaunches.
626626+ * - Manifest no longer parseable: returns null (logs error).
627627+ * - `getTilePreloadPath()` not configured: returns null (logs error).
628628+ */
629629+export async function relaunchTile(tileId: string): Promise<TileLaunchResult | null> {
630630+ const tilePath = getExtensionPath(tileId);
631631+ if (!tilePath) {
632632+ console.error(`[tile-launcher] relaunchTile: no path registered for tile ${tileId}`);
633633+ return null;
634634+ }
635635+636636+ const tilePreloadPath = getTilePreloadPath();
637637+ if (!tilePreloadPath) {
638638+ console.error(`[tile-launcher] relaunchTile: tile preload path not configured — call initialize() first`);
639639+ return null;
640640+ }
641641+642642+ // Re-read manifest from disk so any changes (e.g. during dev reload) are picked up.
643643+ const manifestPath = path.join(tilePath, 'manifest.json');
644644+ const parsed = parseManifestFile(manifestPath);
645645+ if (!parsed || parsed.version !== 'v2' || !parsed.v2) {
646646+ console.error(`[tile-launcher] relaunchTile: cannot re-read v2 manifest for ${tileId} at ${manifestPath}`);
647647+ return null;
648648+ }
649649+ const manifest = parsed.v2;
650650+651651+ // Determine which entry to (re)launch — use the first entry, matching the
652652+ // pattern used in loadV2Tile / tile-compat.
653653+ const entry = manifest.tiles[0];
654654+ if (!entry) {
655655+ console.error(`[tile-launcher] relaunchTile: manifest for ${tileId} has no tile entries`);
656656+ return null;
657657+ }
658658+659659+ // Close the existing window if one is running.
660660+ const existingWin = tileWindows.get(`${tileId}:${entry.id}`);
661661+ if (existingWin && !existingWin.isDestroyed()) {
662662+ // Use unloadTile for clean shutdown (shutdown signal + token revoke + unsubscribe).
663663+ await unloadTile(tileId, entry.id);
664664+ } else {
665665+ // No live window — still revoke any stale tokens from a previous run.
666666+ revokeTokensForTile(tileId, entry.id);
667667+ loadedManifests.delete(tileId);
668668+ }
669669+670670+ DEBUG && console.log(`[tile-launcher] Relaunching ${tileId}:${entry.id} from ${tilePath}`);
671671+672672+ return launchTile({
673673+ tilePath,
674674+ manifest,
675675+ preloadPath: tilePreloadPath,
676676+ entryId: entry.id,
677677+ });
612678}
613679614680// ─── Test Hooks ──────────────────────────────────────────────────────