Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions backend/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ const COMMON_PATH_ROOTS = new Set(['tmp', 'var', 'usr', 'etc', 'home', 'users',
const SKILL_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
const MODULE_SDK_VERSION = '1.6.0';
const MODULE_TASK_STORAGE_KEY = 'chatraw_module_tasks_v1';
const SHELL_PAGE_STORAGE_KEY = 'chatraw_shell_page_v1';
const MODULE_TERMINAL_STATES = new Set(['succeeded', 'failed', 'cancelled']);
const PLUGIN_WORKSPACE_PLACEMENTS = Object.freeze([
'right',
Expand Down Expand Up @@ -1307,7 +1308,8 @@ function app() {
this.initCrossTabStateSync();
// Note: favicon is updated by loadLogo() which is called from loadSettings()
// Initialize plugin system
this.initPluginSystem();
await this.initPluginSystem();
this.restorePluginWorkspacePage();
await this.initResidentIntegrations();
await this.resumeModuleTasks();
},
Expand Down Expand Up @@ -1529,6 +1531,75 @@ function app() {
if (this.pluginWorkspace.show) this.closeActivePluginWorkspace();
},

storedPluginWorkspacePage() {
let page = null;
try {
page = JSON.parse(sessionStorage.getItem(SHELL_PAGE_STORAGE_KEY) || 'null');
} catch (_error) {
page = null;
}
if (
!page
|| page.kind !== 'plugin-workspace'
|| typeof page.pluginId !== 'string'
|| !page.pluginId
|| typeof page.panelId !== 'string'
|| !PLUGIN_WORKSPACE_PANEL_ID_PATTERN.test(page.panelId)
|| !PLUGIN_WORKSPACE_PLACEMENTS.includes(page.placement)
|| Object.keys(page).some(
key => !['kind', 'pluginId', 'panelId', 'placement'].includes(key)
)
) {
this.forgetPluginWorkspacePage();
return null;
}
return page;
},

rememberPluginWorkspacePage(pluginId, panelId, placement) {
try {
sessionStorage.setItem(SHELL_PAGE_STORAGE_KEY, JSON.stringify({
kind: 'plugin-workspace',
pluginId,
panelId,
placement
}));
} catch (_error) {
// A blocked session store must not prevent the page from opening.
}
},

forgetPluginWorkspacePage() {
try {
sessionStorage.removeItem(SHELL_PAGE_STORAGE_KEY);
} catch (_error) {
// The visible page can still close when storage is unavailable.
}
},

restorePluginWorkspacePage() {
const page = this.storedPluginWorkspacePage();
if (!page) return false;
const definition = this.pluginWorkspaceDefinitions[
this.pluginWorkspaceKey(page.pluginId, page.panelId)
];
if (!definition || !definition.placements.includes(page.placement)) {
this.forgetPluginWorkspacePage();
return false;
}
try {
return this.openPluginWorkspacePanel(
page.panelId,
{ placement: page.placement },
page.pluginId
);
} catch (error) {
this.forgetPluginWorkspacePage();
console.warn('[Plugin Workspace restore]', error);
return false;
}
},

toggleAgent() {
if (!this.agentOpen) {
this.showSettings = false;
Expand Down Expand Up @@ -5287,6 +5358,7 @@ function app() {
placement
);
}
this.rememberPluginWorkspacePage(owner, panelId, placement);
return true;
}
if (this.pluginWorkspace.show) {
Expand Down Expand Up @@ -5340,6 +5412,7 @@ function app() {
};
const returnFocus = this._pluginWorkspaceReturnFocus;
this._pluginWorkspaceReturnFocus = null;
this.forgetPluginWorkspacePage();
this.restorePluginWorkspaceFocus(returnFocus);
throw error;
} finally {
Expand All @@ -5352,6 +5425,7 @@ function app() {
placement
);
}
this.rememberPluginWorkspacePage(owner, panelId, placement);
return true;
},

Expand Down Expand Up @@ -5403,6 +5477,7 @@ function app() {
placement: null,
dispose: null
};
this.forgetPluginWorkspacePage();
const returnFocus = this._pluginWorkspaceReturnFocus;
this._pluginWorkspaceReturnFocus = null;
if (restoreFocus) {
Expand Down Expand Up @@ -6590,9 +6665,9 @@ function app() {
}
};

// Load enabled plugins
this.loadEnabledPlugins();
this.initPluginRuntimeSync();
// Loading must finish before init() can restore the saved Workspace.
return this.loadEnabledPlugins();
},

initPluginRuntimeSync() {
Expand Down
2 changes: 1 addition & 1 deletion backend/static/app.min.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions backend/static/frontend-assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"entrypoint": "index.html",
"assets": {
"app.js": {
"sha256": "cc4aa6079fd4780a16c66b5a1012792b9014326d4419276d7338c9e94a9f0a3c",
"version": "cc4aa6079fd4780a16c66b5a1012792b9014326d4419276d7338c9e94a9f0a3c"
"sha256": "97e5a4d56d493f21c50511a47044feffc12ea78944d6dbe08b06311eef617fc2",
"version": "97e5a4d56d493f21c50511a47044feffc12ea78944d6dbe08b06311eef617fc2"
},
"app.min.js": {
"sha256": "a57414bca76151d047c2df6e186755b45369de30e0fad53cd75700289175a5bf",
"version": "a57414bca76151d047c2df6e186755b45369de30e0fad53cd75700289175a5bf"
"sha256": "3a3a04e64ffe8a8e377f8605abbd14b90f88ea92e9ec0016a74413d1a849a2e4",
"version": "3a3a04e64ffe8a8e377f8605abbd14b90f88ea92e9ec0016a74413d1a849a2e4"
},
"auth.css": {
"sha256": "f07a31c703b40bdf18566936d997d1ea574d9757113bbeb714040bf7663ee6fe",
Expand Down Expand Up @@ -59,8 +59,8 @@
"version": "e92dc38bfb1d15dbb9d9ca9e645754f68e7c5b224e33c45a17048bd0cf2407a6"
},
"index.html": {
"sha256": "76d787c5477761a729b0e76d98027e2bfb7e0f1610b5a1462ebf823111ef4de0",
"version": "76d787c5477761a729b0e76d98027e2bfb7e0f1610b5a1462ebf823111ef4de0"
"sha256": "3b036329a38bf6a76c4026b8b895b800bd9a1bdd7e8fa9a00a0ba3301210b907",
"version": "3b036329a38bf6a76c4026b8b895b800bd9a1bdd7e8fa9a00a0ba3301210b907"
},
"landing-field.svg": {
"sha256": "b44d1d8b331f8b7d22bb1127a2c49e4e5d76fd38769fb7ce60c5d533b7f71378",
Expand Down
4 changes: 2 additions & 2 deletions backend/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<!-- Performance: Preload Critical Resources -->
<link rel="preload" href="fonts/remixicon/remixicon.woff2?t=1769685282643" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="styles.min.css?v=776f0101f8816ad42ab1f5862bba9a9e6c4224df14965fe2a3c5ee0a67bb83b0" as="style">
<link rel="preload" href="app.min.js?v=a57414bca76151d047c2df6e186755b45369de30e0fad53cd75700289175a5bf" as="script">
<link rel="preload" href="app.min.js?v=3a3a04e64ffe8a8e377f8605abbd14b90f88ea92e9ec0016a74413d1a849a2e4" as="script">
<link rel="preload" href="vendor/alpine.min.js?v=c8fa8ff457abdcd212f37a07ef2f292c999011dffabcaa577fb1e1e0076ca658" as="script">
<link rel="preload" href="vendor/marked.min.js?v=6002af63485b043fa60ddaba1b34363b98d2a8b2c63b607004f3a2405a8a053a" as="script">
<link rel="preload" href="vendor/purify.min.js?v=c45ba939765574f96cbf35ee9b6d89f73756a17921814425e74b82f7c54603ce" as="script">
Expand Down Expand Up @@ -1883,6 +1883,6 @@ <h3 class="module-task-section-title" x-text="t('artifacts')"></h3>
<div class="plugin-fullscreen-content" x-html="pluginFullscreenModal.content"></div>
</div>

<script src="app.min.js?v=a57414bca76151d047c2df6e186755b45369de30e0fad53cd75700289175a5bf"></script>
<script src="app.min.js?v=3a3a04e64ffe8a8e377f8605abbd14b90f88ea92e9ec0016a74413d1a849a2e4"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions frontend-tests/hub-shell.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ test('catalog availability requires live main-placement panel registration', ()
assert.match(app, /const placement = 'main'/);
});

test('initialization restores the saved page only after enabled Plugins load', () => {
const initBody = app.slice(
app.indexOf('async init() {'),
app.indexOf('\n initCrossTabStateSync()', app.indexOf('async init() {'))
);
assert.match(
initBody,
/await this\.initPluginSystem\(\);[\s\S]*this\.restorePluginWorkspacePage\(\);/
);
const pluginInitBody = app.slice(
app.indexOf('initPluginSystem() {'),
app.indexOf('\n initPluginRuntimeSync()', app.indexOf('initPluginSystem() {'))
);
assert.match(
pluginInitBody,
/this\.initPluginRuntimeSync\(\);[\s\S]*return this\.loadEnabledPlugins\(\);/
);
});

test('installed plugins without a declared icon do not request a missing resource', () => {
assert.match(html, /<template x-if="plugin\.icon">/);
assert.match(html, /x-show="!plugin\.icon"/);
Expand Down
67 changes: 67 additions & 0 deletions frontend-tests/plugin-workspace.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,73 @@ test('workspace API mounts interactive DOM and closes with one disposal', () =>
assert.equal(state.disposals, 1);
});

test('workspace page survives refresh state and restores after Plugin registration', () => {
const first = createHost();
const firstState = counters();
first.ui.registerWorkspacePanel(
panelDefinition('refresh-safe', firstState),
'plugin-one'
);
first.ui.openWorkspacePanel(
'refresh-safe',
{ placement: 'right' },
'plugin-one'
);
const storedPage = first.dom.window.sessionStorage.getItem(
'chatraw_shell_page_v1'
);
assert.deepEqual(JSON.parse(storedPage), {
kind: 'plugin-workspace',
pluginId: 'plugin-one',
panelId: 'refresh-safe',
placement: 'right'
});

const refreshed = createHost();
refreshed.dom.window.sessionStorage.setItem(
'chatraw_shell_page_v1',
storedPage
);
const refreshedState = counters();
refreshed.ui.registerWorkspacePanel(
panelDefinition('refresh-safe', refreshedState),
'plugin-one'
);

assert.equal(refreshed.host.restorePluginWorkspacePage(), true);
assert.equal(refreshed.host.pluginWorkspace.show, true);
assert.equal(refreshed.host.pluginWorkspace.pluginId, 'plugin-one');
assert.equal(refreshed.host.pluginWorkspace.panelId, 'refresh-safe');
assert.equal(refreshed.host.pluginWorkspace.placement, 'right');
assert.equal(refreshedState.mounts, 1);

refreshed.host.returnHome();
assert.equal(
refreshed.dom.window.sessionStorage.getItem('chatraw_shell_page_v1'),
null
);
});

test('workspace refresh state fails closed when the saved panel is unavailable', () => {
const { dom, host } = createHost();
dom.window.sessionStorage.setItem(
'chatraw_shell_page_v1',
JSON.stringify({
kind: 'plugin-workspace',
pluginId: 'plugin-one',
panelId: 'removed-panel',
placement: 'main'
})
);

assert.equal(host.restorePluginWorkspacePage(), false);
assert.equal(host.pluginWorkspace.show, false);
assert.equal(
dom.window.sessionStorage.getItem('chatraw_shell_page_v1'),
null
);
});

test('workspace open options distinguish omission from invalid explicit values', () => {
const { host, ui } = createHost();
const state = counters();
Expand Down
Loading