Release 0.2.0 #4

Merged
dguiducci merged 96 commits from main into release 2026-08-17 18:07:20 +01:00
7 changed files with 140 additions and 3 deletions
Showing only changes of commit d3fd9bd3af - Show all commits
+2 -1
View File
@@ -4,7 +4,7 @@ This folder is written for **you, the assistant**, not for the human directly. I
Keep answers grounded in what's actually enabled and configured for this instance — check with the relevant tool (e.g. list installed/enabled plugins) rather than assuming everything described here is turned on. A feature documented here may not be enabled on this particular instance. Keep answers grounded in what's actually enabled and configured for this instance — check with the relevant tool (e.g. list installed/enabled plugins) rather than assuming everything described here is turned on. A feature documented here may not be enabled on this particular instance.
This index will grow over time. Right now it covers memory, projects, background tasks, system agents, access grants, connectors, voice input and plugins; more sections (agents, security groups, shared folders…) will be added later. This index will grow over time. Right now it covers the interface, memory, projects, background tasks, system agents, access grants, connectors, voice input and plugins; more sections (agents, security groups, shared folders…) will be added later.
## Features ## Features
@@ -18,6 +18,7 @@ This index will grow over time. Right now it covers memory, projects, background
| [access.md](access.md) | Who can use which plugin or connector: the open default, removing access per person, and the role switch that keeps children out of it | | [access.md](access.md) | Who can use which plugin or connector: the open default, removing access per person, and the role switch that keeps children out of it |
| [connectors.md](connectors.md) | Connectors (MCP servers): shared vs per-user, setting one up in the UI, the sign-in and QR-pairing flows, and what to do when one is not working | | [connectors.md](connectors.md) | Connectors (MCP servers): shared vs per-user, setting one up in the UI, the sign-in and QR-pairing flows, and what to do when one is not working |
| [voice.md](voice.md) | Voice input: configuring a transcription model, and why the microphone button does nothing unless the page is served over HTTPS or localhost | | [voice.md](voice.md) | Voice input: configuring a transcription model, and why the microphone button does nothing unless the page is served over HTTPS or localhost |
| [interface.md](interface.md) | The desktop interface: collapsing the sidebar to an icon-only strip to make room for documents |
## Plugins ## Plugins
+11
View File
@@ -0,0 +1,11 @@
# The desktop interface
The web app's left sidebar is the main navigation: chats, inbox, projects, tasks, and (for admins) the configuration pages.
## Collapsing the sidebar
The double-chevron button at the top of the sidebar, next to the app icon, collapses the menu to a narrow strip of **icons only**. Every icon stays clickable and leads to the same page as before; hovering an icon shows its name as a tooltip. Sub-items that have no icon of their own — the Task Manager sections, the recent-projects list — disappear while collapsed, and so do the section headers. The inbox unread count survives as a small red badge on the inbox icon.
Click the button again (it now points right) to bring the full menu back. The choice is remembered in the browser, so the sidebar comes back the way it was left.
Collapsing is purely visual and per browser window: it changes nothing about what the user can access, only how much room the menu takes — useful when working on documents in the file viewer or on a project board.
+37 -2
View File
@@ -61,6 +61,9 @@ const GROUPS = [
]; ];
const COLLAPSE_KEY = 'sidebar-collapsed'; const COLLAPSE_KEY = 'sidebar-collapsed';
// Icon-only (minimized) sidebar, persisted per browser: only the menu icons stay
// visible, still clickable; labels, section headers and submenus disappear.
const MINIMIZE_KEY = 'sidebar-minimized';
// The projects NAV entry, surfaced in the simplified interface too (projects are // The projects NAV entry, surfaced in the simplified interface too (projects are
// membership-gated, not capability-gated, so a simple-mode member can own/share // membership-gated, not capability-gated, so a simple-mode member can own/share
@@ -78,6 +81,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
_me: { state: true }, _me: { state: true },
_pluginPages: { state: true }, _pluginPages: { state: true },
_collapsed: { state: true }, _collapsed: { state: true },
_minimized: { state: true },
}; };
constructor() { constructor() {
@@ -91,6 +95,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
this._me = null; this._me = null;
this._pluginPages = []; this._pluginPages = [];
this._collapsed = { config: true, dev: true }; this._collapsed = { config: true, dev: true };
this._minimized = false;
} }
connectedCallback() { connectedCallback() {
@@ -124,6 +129,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
this._pollTimer = setInterval(() => this._pollInbox(), 60000); this._pollTimer = setInterval(() => this._pollInbox(), 60000);
window.addEventListener('inbox-changed', () => this._pollInbox()); window.addEventListener('inbox-changed', () => this._pollInbox());
this._loadCollapsed(); this._loadCollapsed();
try { this._minimized = localStorage.getItem(MINIMIZE_KEY) === '1'; } catch { /* keep default */ }
this._loadDebugMode(); this._loadDebugMode();
this._loadRecentProjects(); this._loadRecentProjects();
this._loadMe(); this._loadMe();
@@ -147,6 +153,19 @@ export class AppSidebar extends I18nMixin(LightElement) {
clearInterval(this._pollTimer); clearInterval(this._pollTimer);
} }
// The minimized state is a class on the host element (`app-sidebar`), since
// the width itself shrinks and the stylesheet keys off it.
updated(changed) {
if (changed.has('_minimized')) {
this.classList.toggle('sidebar-minimized', this._minimized);
try { localStorage.setItem(MINIMIZE_KEY, this._minimized ? '1' : '0'); } catch { /* ignore */ }
}
}
_toggleMinimized() {
this._minimized = !this._minimized;
}
// Persisted per-section collapse state. Defaults (config + dev closed) apply // Persisted per-section collapse state. Defaults (config + dev closed) apply
// until the user toggles a section, then their choice is remembered. // until the user toggles a section, then their choice is remembered.
_loadCollapsed() { _loadCollapsed() {
@@ -314,7 +333,9 @@ export class AppSidebar extends I18nMixin(LightElement) {
_renderGroup(group) { _renderGroup(group) {
const entries = this._entriesForGroup(group.id); const entries = this._entriesForGroup(group.id);
if (!entries.length) return nothing; // empty section → hidden if (!entries.length) return nothing; // empty section → hidden
const collapsed = group.collapsible && this._collapsed[group.id]; // Minimized: section labels are hidden, so a collapsible section could never
// be re-opened — render every entry (icons only) regardless of its state.
const collapsed = !this._minimized && group.collapsible && this._collapsed[group.id];
const header = group.collapsible const header = group.collapsible
? html` ? html`
<button class="sidebar-section-label is-collapsible" <button class="sidebar-section-label is-collapsible"
@@ -345,6 +366,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
_renderStdLink(item) { _renderStdLink(item) {
return html` return html`
<a href="#${item.id}" class="sidebar-link ${this._isActive(item) ? 'active' : ''}" <a href="#${item.id}" class="sidebar-link ${this._isActive(item) ? 'active' : ''}"
title=${this._minimized ? t(item.labelKey) : nothing}
@click=${(e) => this._togglePage(item.id, e)}> @click=${(e) => this._togglePage(item.id, e)}>
<i class="bi bi-${item.icon}"></i> <i class="bi bi-${item.icon}"></i>
<span class="sidebar-link-name">${t(item.labelKey)}</span> <span class="sidebar-link-name">${t(item.labelKey)}</span>
@@ -354,6 +376,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
_renderHome() { _renderHome() {
return html` return html`
<a href="#" class="sidebar-link ${this._activePage === 'home' ? 'active' : ''}" <a href="#" class="sidebar-link ${this._activePage === 'home' ? 'active' : ''}"
title=${this._minimized ? t('nav.chat') : nothing}
@click=${(e) => this._togglePage('home', e)}> @click=${(e) => this._togglePage('home', e)}>
<i class="bi bi-chat-dots"></i> <i class="bi bi-chat-dots"></i>
<span class="sidebar-link-name">${t('nav.chat')}</span> <span class="sidebar-link-name">${t('nav.chat')}</span>
@@ -361,13 +384,18 @@ export class AppSidebar extends I18nMixin(LightElement) {
} }
_renderInbox(item) { _renderInbox(item) {
const min = this._minimized;
return html` return html`
<a href="#inbox" class="sidebar-link ${this._isActive(item) ? 'active' : ''}" <a href="#inbox" class="sidebar-link ${this._isActive(item) ? 'active' : ''}"
title=${min ? t('nav.inbox') : nothing}
@click=${(e) => this._togglePage('inbox', e)}> @click=${(e) => this._togglePage('inbox', e)}>
<i class="bi bi-inbox"></i> <i class="bi bi-inbox"></i>
${min && this._inboxCount > 0
? html`<span class="sidebar-icon-badge">${this._inboxCount}</span>`
: nothing}
<span class="sidebar-link-name"> <span class="sidebar-link-name">
${t('nav.inbox')} ${t('nav.inbox')}
${this._inboxCount > 0 ${!min && this._inboxCount > 0
? html`<span class="badge bg-danger ms-1" style="font-size:0.65rem">${this._inboxCount}</span>` ? html`<span class="badge bg-danger ms-1" style="font-size:0.65rem">${this._inboxCount}</span>`
: ''} : ''}
</span> </span>
@@ -379,6 +407,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
return html` return html`
<a href="#${route}" <a href="#${route}"
class="sidebar-link ${this._activePage === route ? 'active' : ''}" class="sidebar-link ${this._activePage === route ? 'active' : ''}"
title=${this._minimized ? p.title : nothing}
@click=${(e) => this._togglePage(route, e)}> @click=${(e) => this._togglePage(route, e)}>
<i class="bi bi-${p.icon}"></i> <i class="bi bi-${p.icon}"></i>
<span class="sidebar-link-name">${p.title}</span> <span class="sidebar-link-name">${p.title}</span>
@@ -391,6 +420,7 @@ export class AppSidebar extends I18nMixin(LightElement) {
return html` return html`
<a href="#tasks/cron" <a href="#tasks/cron"
class="sidebar-link ${active ? 'active' : ''}" class="sidebar-link ${active ? 'active' : ''}"
title=${this._minimized ? t('nav.tasks') : nothing}
@click=${(e) => this._openTaskManager(e)}> @click=${(e) => this._openTaskManager(e)}>
<i class="bi bi-lightning-charge"></i> <i class="bi bi-lightning-charge"></i>
<span class="sidebar-link-name">${t('nav.tasks')}</span> <span class="sidebar-link-name">${t('nav.tasks')}</span>
@@ -454,6 +484,11 @@ export class AppSidebar extends I18nMixin(LightElement) {
<div class="sidebar-brand"> <div class="sidebar-brand">
<img src="/assets/icons/icon-1024.png" alt="" class="sidebar-brand-icon" /> <img src="/assets/icons/icon-1024.png" alt="" class="sidebar-brand-icon" />
<span>${t('topbar.brand')}</span> <span>${t('topbar.brand')}</span>
<button class="sidebar-collapse-btn"
title=${this._minimized ? t('nav.expand_sidebar') : t('nav.collapse_sidebar')}
@click=${() => this._toggleMinimized()}>
<i class="bi bi-chevron-double-${this._minimized ? 'right' : 'left'}"></i>
</button>
</div> </div>
<hr class="sidebar-divider" /> <hr class="sidebar-divider" />
+84
View File
@@ -9,6 +9,7 @@ app-sidebar {
flex-shrink: 0; flex-shrink: 0;
padding: 1.25rem 0 1rem; padding: 1.25rem 0 1rem;
overflow-y: auto; overflow-y: auto;
transition: width 0.15s ease;
} }
.sidebar-brand { .sidebar-brand {
@@ -275,3 +276,86 @@ app-sidebar {
display: flex; display: flex;
align-items: center; align-items: center;
} }
/* ── Collapse toggle ── */
.sidebar-collapse-btn {
margin-left: auto;
background: none;
border: none;
color: var(--sidebar-label-color);
padding: 0.2rem 0.35rem;
border-radius: 0.4rem;
cursor: pointer;
font-size: 0.78rem;
line-height: 1;
flex-shrink: 0;
transition: background 0.12s, color 0.12s;
}
.sidebar-collapse-btn:hover {
background: var(--sidebar-hover);
color: var(--sidebar-text-active);
}
/* ── Minimized (icon-only) mode ── */
app-sidebar.sidebar-minimized {
width: 58px;
}
app-sidebar.sidebar-minimized .sidebar-brand {
flex-direction: column;
gap: 0.45rem;
padding: 0 0.25rem 1rem;
align-items: center;
}
app-sidebar.sidebar-minimized .sidebar-brand > span {
display: none;
}
app-sidebar.sidebar-minimized .sidebar-collapse-btn {
margin-left: 0;
}
app-sidebar.sidebar-minimized .sidebar-divider {
margin: 0.85rem 0.5rem;
}
/* Section headers and submenus (Task Manager sections, recent projects) have
no icon of their own — they disappear; every top-level icon stays clickable. */
app-sidebar.sidebar-minimized .sidebar-section-label,
app-sidebar.sidebar-minimized .sidebar-submenu {
display: none;
}
app-sidebar.sidebar-minimized .sidebar-nav {
padding: 0 0.4rem;
}
app-sidebar.sidebar-minimized .sidebar-link {
justify-content: center;
padding: 0.45rem;
position: relative;
}
app-sidebar.sidebar-minimized .sidebar-link-name,
app-sidebar.sidebar-minimized .sidebar-link-chevron {
display: none;
}
/* Inbox count, re-homed onto the icon when the label is hidden. */
.sidebar-icon-badge {
position: absolute;
top: 0;
right: 2px;
background: var(--bs-danger, #dc3545);
color: #fff;
font-size: 0.58rem;
font-weight: 700;
line-height: 1;
padding: 2px 4px;
border-radius: 999px;
pointer-events: none;
}
+2
View File
@@ -24,6 +24,8 @@ export default {
'nav.config': 'Settings', 'nav.config': 'Settings',
'nav.llm_requests': 'LLM Requests', 'nav.llm_requests': 'LLM Requests',
'nav.system_agents': 'System agents', 'nav.system_agents': 'System agents',
'nav.collapse_sidebar': 'Collapse menu',
'nav.expand_sidebar': 'Expand menu',
// ── Top bar ──────────────────────────────────────────────────────────────── // ── Top bar ────────────────────────────────────────────────────────────────
'topbar.profile': 'Profile', 'topbar.profile': 'Profile',
+2
View File
@@ -24,6 +24,8 @@ export default {
'nav.config': 'Paramètres', 'nav.config': 'Paramètres',
'nav.llm_requests': 'Requêtes LLM', 'nav.llm_requests': 'Requêtes LLM',
'nav.system_agents': 'Agents système', 'nav.system_agents': 'Agents système',
'nav.collapse_sidebar': 'Réduire le menu',
'nav.expand_sidebar': 'Déplier le menu',
// ── Top bar ──────────────────────────────────────────────────────────────── // ── Top bar ────────────────────────────────────────────────────────────────
'topbar.profile': 'Profil', 'topbar.profile': 'Profil',
+2
View File
@@ -24,6 +24,8 @@ export default {
'nav.config': 'Impostazioni', 'nav.config': 'Impostazioni',
'nav.llm_requests': 'Richieste LLM', 'nav.llm_requests': 'Richieste LLM',
'nav.system_agents': 'Agenti di sistema', 'nav.system_agents': 'Agenti di sistema',
'nav.collapse_sidebar': 'Comprimi il menu',
'nav.expand_sidebar': 'Espandi il menu',
// ── Barra superiore ──────────────────────────────────────────────────────── // ── Barra superiore ────────────────────────────────────────────────────────
'topbar.profile': 'Profilo', 'topbar.profile': 'Profilo',