Changeset 0.37.19 (#232)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
// then post-processes wikilinks into clickable chips
|
||||
// and transclusion blocks.
|
||||
// Independently importable.
|
||||
//
|
||||
// NOTE (CR P3-17): Uses raw DOM (innerHTML, querySelector, addEventListener)
|
||||
// for post-render wikilink processing. This is intentional — Preact renders
|
||||
// the initial HTML, then this module post-processes specific elements for
|
||||
// interactive behavior. The lifecycle is managed by the parent component.
|
||||
|
||||
const WIKILINK_RE = /(!?)\[\[([^\[\]|]+?)(?:\|([^\]]+?))?\]\]/g;
|
||||
|
||||
|
||||
@@ -257,6 +257,8 @@ export function useNotes(opts = {}) {
|
||||
toast('Note updated', 'success');
|
||||
setEditorMode('read');
|
||||
_setView('reader');
|
||||
// Notify sidebar to refresh folders/tags (folder or tags may have changed)
|
||||
sw.emit('note.updated', { id: editingId }, { localOnly: true });
|
||||
// Reload backlinks
|
||||
try {
|
||||
const bl = await api().backlinks(editingId);
|
||||
@@ -269,6 +271,8 @@ export function useNotes(opts = {}) {
|
||||
toast('Note created', 'success');
|
||||
setEditorMode('read');
|
||||
_setView('reader');
|
||||
// Notify sidebar to refresh folders/tags
|
||||
sw.emit('note.created', { id: created.id }, { localOnly: true });
|
||||
}
|
||||
graphDirtyRef.current = true;
|
||||
return true;
|
||||
@@ -289,6 +293,7 @@ export function useNotes(opts = {}) {
|
||||
setView('list');
|
||||
refreshList();
|
||||
_loadFolders();
|
||||
sw.emit('note.deleted', { id: editingId }, { localOnly: true });
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
}, [editingId, setView, refreshList, _loadFolders]);
|
||||
|
||||
@@ -417,7 +422,9 @@ export function useNotes(opts = {}) {
|
||||
|
||||
// Ctrl/Cmd + N: new note
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||
// Only prevent when notes pane is focused context
|
||||
// DOM presence check gates shortcut to notes surface (CR P3-19).
|
||||
// If notes and chat are ever composed on the same page, revisit
|
||||
// with a focus-based check or sw.shell.activeSurface flag.
|
||||
if (document.querySelector('.sw-notes-pane')) {
|
||||
e.preventDefault();
|
||||
newNote();
|
||||
|
||||
@@ -97,6 +97,19 @@ export async function boot() {
|
||||
sw.confirm = confirm;
|
||||
sw.prompt = prompt;
|
||||
|
||||
// Shell — layout utilities (decouples surface code from shell DOM)
|
||||
sw.shell = Object.freeze({
|
||||
/** CSS transform scale on #surfaceInner (appearance zoom). */
|
||||
getScale() {
|
||||
const el = document.getElementById('surfaceInner');
|
||||
if (!el) return 1;
|
||||
const t = getComputedStyle(el).transform;
|
||||
if (!t || t === 'none') return 1;
|
||||
const m = t.match(/matrix\(([^,]+)/);
|
||||
return m ? parseFloat(m[1]) || 1 : 1;
|
||||
},
|
||||
});
|
||||
|
||||
// Toast — dynamic import to avoid module resolution issues
|
||||
try {
|
||||
const toastMod = await import('../primitives/toast.js');
|
||||
@@ -139,7 +152,7 @@ export async function boot() {
|
||||
};
|
||||
|
||||
// Marker for idempotency
|
||||
sw._sdk = '0.37.14';
|
||||
sw._sdk = '0.37.19';
|
||||
|
||||
// 8. Expose globally
|
||||
window.sw = sw;
|
||||
|
||||
@@ -172,7 +172,7 @@ function ChatDashboard({ onNewChat, onCreateChannel, items, onNavigate }) {
|
||||
const _onDmSelect = useCallback((user) => {
|
||||
setShowDmPicker(false);
|
||||
const name = user.display_name || user.username;
|
||||
onCreateChannel('dm', 'DM: ' + name, { participant: user.username || user.id });
|
||||
onCreateChannel('dm', 'DM: ' + name, { participant: user.id });
|
||||
}, [onCreateChannel]);
|
||||
|
||||
return html`
|
||||
@@ -186,14 +186,16 @@ function ChatDashboard({ onNewChat, onCreateChannel, items, onNavigate }) {
|
||||
<span class="sw-chat-dashboard__card-icon">\u{1F4AC}</span>
|
||||
<span class="sw-chat-dashboard__card-label">New AI Chat</span>
|
||||
</button>
|
||||
${sw.can('channel.create') && html`
|
||||
<button class="sw-chat-dashboard__card" onClick=${() => _create('channel')}>
|
||||
<span class="sw-chat-dashboard__card-icon">#</span>
|
||||
<span class="sw-chat-dashboard__card-label">New Channel</span>
|
||||
</button>
|
||||
</button>`}
|
||||
${sw.can('channel.create') && html`
|
||||
<button class="sw-chat-dashboard__card" onClick=${() => _create('group')}>
|
||||
<span class="sw-chat-dashboard__card-icon">\u{1F465}</span>
|
||||
<span class="sw-chat-dashboard__card-label">New Group</span>
|
||||
</button>
|
||||
</button>`}
|
||||
<button class="sw-chat-dashboard__card" onClick=${() => _create('dm')}>
|
||||
<span class="sw-chat-dashboard__card-icon">\u{1F4E8}</span>
|
||||
<span class="sw-chat-dashboard__card-label">Direct Message</span>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function SidebarItems({
|
||||
const _openMenu = useCallback((e, type, item) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const scale = _getScale();
|
||||
const scale = sw.shell.getScale();
|
||||
// The dots button may be display:none (zero rect) — fall back to parent
|
||||
let rect = e.currentTarget.getBoundingClientRect();
|
||||
if (!rect.width && !rect.height) {
|
||||
@@ -97,7 +97,7 @@ export function SidebarItems({
|
||||
const _bodyContextMenu = useCallback((e) => {
|
||||
if (e.target.closest('.sw-chat-surface__item') || e.target.closest('.sw-chat-surface__folder-header')) return;
|
||||
e.preventDefault();
|
||||
const scale = _getScale();
|
||||
const scale = sw.shell.getScale();
|
||||
setContextMenu({ type: 'body', item: null, x: e.clientX / scale, y: e.clientY / scale });
|
||||
}, []);
|
||||
|
||||
@@ -356,16 +356,6 @@ function ChannelItem({ channel, activeId, onSelect, onOpenMenu, onDragStart, ind
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// Read the CSS scale transform on .surface-inner (set by appearance zoom).
|
||||
// Context menus use position:fixed which breaks under transforms,
|
||||
// so we divide getBoundingClientRect values by scale to compensate.
|
||||
function _getScale() {
|
||||
const el = document.getElementById('surfaceInner');
|
||||
if (!el) return 1;
|
||||
const t = getComputedStyle(el).transform;
|
||||
if (!t || t === 'none') return 1;
|
||||
const m = t.match(/matrix\(([^,]+)/);
|
||||
return m ? parseFloat(m[1]) || 1 : 1;
|
||||
}
|
||||
// Scale detection moved to sw.shell.getScale() in v0.37.19 (CR P3-3).
|
||||
|
||||
// Folder count = direct children only (subfolders + channels in this folder).
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { SidebarItems } from './sidebar-chats.js';
|
||||
import { UserMenu } from '../../shell/user-menu.js';
|
||||
import { UserPicker } from '../../primitives/user-picker.js';
|
||||
|
||||
const html = window.html;
|
||||
const { useState, useCallback, useRef, useEffect } = window.hooks;
|
||||
@@ -50,6 +51,7 @@ const FOLDER_PLUS_SVG = html`
|
||||
*/
|
||||
export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChannel, onDeleteChat, onCollapse }) {
|
||||
const [newMenuOpen, setNewMenuOpen] = useState(false);
|
||||
const [showDmPicker, setShowDmPicker] = useState(false);
|
||||
const newMenuRef = useRef(null);
|
||||
|
||||
const _onSearch = useCallback((e) => {
|
||||
@@ -82,14 +84,19 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne
|
||||
if (type === 'direct') {
|
||||
onNewChat();
|
||||
} else if (type === 'dm') {
|
||||
const who = await window.sw.prompt('Username to DM:', '');
|
||||
if (who && who.trim()) onCreateChannel('dm', 'DM: ' + who.trim(), { participant: who.trim() });
|
||||
setShowDmPicker(true);
|
||||
} else {
|
||||
const name = await window.sw.prompt((type === 'channel' ? 'Channel' : 'Group') + ' name:', '');
|
||||
if (name && name.trim()) onCreateChannel(type, name.trim());
|
||||
}
|
||||
}, [onNewChat, onCreateChannel]);
|
||||
|
||||
const _onDmSelect = useCallback((user) => {
|
||||
setShowDmPicker(false);
|
||||
const name = user.display_name || user.username;
|
||||
onCreateChannel('dm', 'DM: ' + name, { participant: user.id });
|
||||
}, [onCreateChannel]);
|
||||
|
||||
return html`
|
||||
<div class="sw-chat-surface__sidebar" role="navigation" aria-label="Chat sidebar">
|
||||
${/* Header: new chat + collapse */''}
|
||||
@@ -107,12 +114,12 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne
|
||||
<button onClick=${() => _newChannel('direct')}>
|
||||
<span class="sw-chat-surface__new-menu-icon">\u{1F4AC}</span> AI Chat
|
||||
</button>
|
||||
<button onClick=${() => _newChannel('channel')}>
|
||||
${sw.can('channel.create') && html`<button onClick=${() => _newChannel('channel')}>
|
||||
<span class="sw-chat-surface__new-menu-icon">#</span> Channel
|
||||
</button>
|
||||
<button onClick=${() => _newChannel('group')}>
|
||||
</button>`}
|
||||
${sw.can('channel.create') && html`<button onClick=${() => _newChannel('group')}>
|
||||
<span class="sw-chat-surface__new-menu-icon">\u{1F465}</span> Group
|
||||
</button>
|
||||
</button>`}
|
||||
<button onClick=${() => _newChannel('dm')}>
|
||||
<span class="sw-chat-surface__new-menu-icon">\u{1F4E8}</span> Direct Message
|
||||
</button>
|
||||
@@ -132,6 +139,17 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${showDmPicker && html`
|
||||
<div style="padding: 6px 8px;">
|
||||
<${UserPicker}
|
||||
onSelect=${_onDmSelect}
|
||||
placeholder="Search for a user\u2026"
|
||||
autoFocus=${true} />
|
||||
<button class="btn-small"
|
||||
style="margin-top: 4px; width: 100%;"
|
||||
onClick=${() => setShowDmPicker(false)}>Cancel</button>
|
||||
</div>`}
|
||||
|
||||
${/* Search */''}
|
||||
<div class="sw-chat-surface__search-wrap">
|
||||
<span class="sw-chat-surface__search-icon">${SEARCH_SVG}</span>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* Reads globals:
|
||||
* __SECTION__ — active section name (string)
|
||||
* __BASE__ — base path
|
||||
* __PAGE_DATA__ — { BYOKEnabled, ... } from Go template
|
||||
*
|
||||
* Policy flags (BYOK, personas) come from sw.auth.policies,
|
||||
* populated at SDK boot via GET /api/v1/profile/permissions.
|
||||
*
|
||||
* Layout: topbar + left nav + content area (same CSS classes as before).
|
||||
* All sections are native Preact components loaded lazily.
|
||||
@@ -73,21 +75,21 @@ const SECTION_TITLES = {
|
||||
function SettingsSurface() {
|
||||
const BASE = window.__BASE__ || '';
|
||||
const section = window.__SECTION__ || 'general';
|
||||
const pageData = window.__PAGE_DATA__ || {};
|
||||
|
||||
const [byokEnabled, setByokEnabled] = useState(!!pageData.BYOKEnabled);
|
||||
const [personasEnabled, setPersonasEnabled] = useState(!!pageData.UserPersonasEnabled);
|
||||
const policies = sw.auth?.policies || {};
|
||||
const [byokEnabled, setByokEnabled] = useState(policies.allow_user_byok === 'true');
|
||||
const [personasEnabled, setPersonasEnabled] = useState(policies.allow_user_personas === 'true');
|
||||
const [SectionComponent, setSectionComponent] = useState(null);
|
||||
|
||||
// Fetch policies if not provided by template
|
||||
// Update when permissions refresh (e.g. after boot or policy change)
|
||||
useEffect(() => {
|
||||
if (!pageData.BYOKEnabled || !pageData.UserPersonasEnabled) {
|
||||
sw.api.admin?.settings?.public?.().then(data => {
|
||||
const policies = data?.policies || {};
|
||||
if (policies.allow_user_byok === 'true') setByokEnabled(true);
|
||||
if (policies.allow_user_personas === 'true') setPersonasEnabled(true);
|
||||
}).catch(() => {});
|
||||
function _onPermsChanged() {
|
||||
const p = sw.auth?.policies || {};
|
||||
setByokEnabled(p.allow_user_byok === 'true');
|
||||
setPersonasEnabled(p.allow_user_personas === 'true');
|
||||
}
|
||||
sw.on('auth.permissions.changed', _onPermsChanged);
|
||||
return () => sw.off('auth.permissions.changed', _onPermsChanged);
|
||||
}, []);
|
||||
|
||||
// Load the section component
|
||||
|
||||
@@ -101,6 +101,9 @@ export default function KnowledgeSection() {
|
||||
return html`<span class="badge ${cls}">${status}</span>`;
|
||||
}
|
||||
|
||||
const canCreate = sw.can('kb.create');
|
||||
const canWrite = sw.can('kb.write');
|
||||
|
||||
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
|
||||
|
||||
// Detail view
|
||||
@@ -111,8 +114,8 @@ export default function KnowledgeSection() {
|
||||
<h4 style="margin:0;">${detail.name}</h4>
|
||||
${statusBadge(detail.status || 'active')}
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn-small" onClick=${uploadDoc}>Upload Document</button>
|
||||
<button class="btn-small btn-danger" onClick=${() => deleteKb(detail.id)}>Delete KB</button>
|
||||
${canWrite && html`<button class="btn-small" onClick=${uploadDoc}>Upload Document</button>`}
|
||||
${canWrite && html`<button class="btn-small btn-danger" onClick=${() => deleteKb(detail.id)}>Delete KB</button>`}
|
||||
</div>
|
||||
|
||||
${detail.description && html`
|
||||
@@ -132,7 +135,7 @@ export default function KnowledgeSection() {
|
||||
${' '}${statusBadge(d.status || 'pending')}
|
||||
</div>
|
||||
<span class="text-muted" style="font-size:11px;">${fmtDate(d.created_at)}</span>
|
||||
<button class="btn-small btn-danger" onClick=${() => deleteDoc(d.id)}>Delete</button>
|
||||
${canWrite && html`<button class="btn-small btn-danger" onClick=${() => deleteDoc(d.id)}>Delete</button>`}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
@@ -145,9 +148,10 @@ export default function KnowledgeSection() {
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span class="text-muted" style="font-size:12px;">${kbs.length} knowledge base${kbs.length !== 1 ? 's' : ''}</span>
|
||||
<div style="flex:1"></div>
|
||||
${canCreate && html`
|
||||
<button class="btn-md btn-primary" onClick=${() => setShowCreate(!showCreate)}>
|
||||
${showCreate ? 'Cancel' : '+ Create'}
|
||||
</button>
|
||||
</button>`}
|
||||
</div>
|
||||
|
||||
${showCreate && html`
|
||||
|
||||
@@ -85,10 +85,14 @@ export function PersonasSection() {
|
||||
return html`<div class="settings-placeholder">Loading personas\u2026</div>`;
|
||||
}
|
||||
|
||||
const canCreate = sw.can('persona.create');
|
||||
const canManage = sw.can('persona.manage');
|
||||
|
||||
return html`
|
||||
${canCreate && html`
|
||||
<div style="margin-bottom:12px;">
|
||||
<button class="btn-md btn-primary" onClick=${openAdd}>+ New Persona</button>
|
||||
</div>
|
||||
</div>`}
|
||||
|
||||
${showForm && html`
|
||||
<div class="settings-section" style="margin-bottom:16px;">
|
||||
@@ -136,6 +140,7 @@ export function PersonasSection() {
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
${canManage && html`
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button class="icon-btn" title="Edit" onClick=${() => openEdit(p)}>
|
||||
\u{270F}\u{FE0F}
|
||||
@@ -144,7 +149,7 @@ export function PersonasSection() {
|
||||
onClick=${() => del(p)}>
|
||||
\u{1F5D1}
|
||||
</button>
|
||||
</div>
|
||||
</div>`}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
|
||||
Reference in New Issue
Block a user