CRITICAL:
- DocsTab XSS: DOMPurify sanitization on all {@html} output
- File RPC path traversal: guardPath() validates against project CWDs
HIGH:
- SSH injection: spawn /usr/bin/ssh via PTY args, no shell string
- Search XSS: strip HTML, highlight matches client-side with <mark>
- Terminal listener leak: cleanup functions stored + called in onDestroy
- FileBrowser race: request token, discard stale responses
- SearchOverlay race: same request token pattern
- App startup ordering: groups.list chains into active_group restore
- PtyClient timeout: 5-second auth timeout on connect()
- Rule 55: 6 {#if} patterns converted to style:display toggle
MEDIUM:
- Agent persistence: only persist NEW messages (lastPersistedIndex)
- Search errors: typed error response, "Invalid query" UI
- Health store wired: agent events call recordActivity/setProjectStatus
- index.ts SRP: split into 8 domain handler modules (298 lines)
- App.svelte: extracted workspace-store.svelte.ts
- rpc.ts: typed AppRpcHandle, removed `any`
LOW:
- CommandPalette listener wired in App.svelte
- Dead code removed (removeGroup, onDragStart, plugin loaded)
160 lines
4 KiB
Svelte
160 lines
4 KiB
Svelte
<script lang="ts">
|
|
export interface Notification {
|
|
id: number;
|
|
message: string;
|
|
type: 'success' | 'warning' | 'info' | 'error';
|
|
time: string;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
notifications: Notification[];
|
|
onClear: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { open, notifications, onClear, onClose }: Props = $props();
|
|
</script>
|
|
|
|
<!-- Fix #11: display toggle instead of {#if} -->
|
|
<!-- Backdrop to close on outside click -->
|
|
<div class="notif-backdrop" style:display={open ? 'block' : 'none'} role="presentation" onclick={onClose}></div>
|
|
|
|
<div class="notif-drawer" style:display={open ? 'flex' : 'none'} role="complementary" aria-label="Notification history">
|
|
<div class="drawer-header">
|
|
<span class="drawer-title">Notifications</span>
|
|
<button class="clear-btn" onclick={onClear} aria-label="Clear all notifications">
|
|
Clear all
|
|
</button>
|
|
</div>
|
|
|
|
<div class="drawer-body">
|
|
{#each notifications as notif (notif.id)}
|
|
<div class="notif-item" class:success={notif.type === 'success'} class:warning={notif.type === 'warning'} class:error={notif.type === 'error'}>
|
|
<span class="notif-dot" class:success={notif.type === 'success'} class:warning={notif.type === 'warning'} class:error={notif.type === 'error'} aria-hidden="true"></span>
|
|
<div class="notif-content">
|
|
<span class="notif-text">{notif.message}</span>
|
|
<span class="notif-time">{notif.time}</span>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
|
|
{#if notifications.length === 0}
|
|
<div class="notif-empty">No notifications</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.notif-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 90;
|
|
}
|
|
|
|
.notif-drawer {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 2.25rem; /* right-bar width */
|
|
bottom: var(--status-bar-height, 1.5rem);
|
|
width: 18rem;
|
|
background: var(--ctp-mantle);
|
|
border-left: 1px solid var(--ctp-surface0);
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 91;
|
|
box-shadow: -0.25rem 0 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
|
}
|
|
|
|
.drawer-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.625rem 0.75rem;
|
|
border-bottom: 1px solid var(--ctp-surface0);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.drawer-title {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: var(--ctp-text);
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.clear-btn {
|
|
background: transparent;
|
|
border: none;
|
|
font-size: 0.6875rem;
|
|
color: var(--ctp-overlay1);
|
|
cursor: pointer;
|
|
padding: 0.125rem 0.25rem;
|
|
border-radius: 0.25rem;
|
|
font-family: var(--ui-font-family);
|
|
transition: color 0.12s;
|
|
}
|
|
|
|
.clear-btn:hover { color: var(--ctp-text); }
|
|
|
|
.drawer-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
padding: 0.375rem 0;
|
|
}
|
|
|
|
.drawer-body::-webkit-scrollbar { width: 0.25rem; }
|
|
.drawer-body::-webkit-scrollbar-track { background: transparent; }
|
|
.drawer-body::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
|
|
|
.notif-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 0.75rem;
|
|
transition: background 0.1s;
|
|
}
|
|
|
|
.notif-item:hover { background: var(--ctp-surface0); }
|
|
|
|
.notif-dot {
|
|
flex-shrink: 0;
|
|
width: 0.4375rem;
|
|
height: 0.4375rem;
|
|
border-radius: 50%;
|
|
margin-top: 0.3rem;
|
|
background: var(--ctp-overlay1);
|
|
}
|
|
|
|
.notif-dot.success { background: var(--ctp-green); }
|
|
.notif-dot.warning { background: var(--ctp-yellow); }
|
|
.notif-dot.error { background: var(--ctp-red); }
|
|
|
|
.notif-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.125rem;
|
|
}
|
|
|
|
.notif-text {
|
|
font-size: 0.75rem;
|
|
color: var(--ctp-text);
|
|
line-height: 1.4;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.notif-time {
|
|
font-size: 0.625rem;
|
|
color: var(--ctp-overlay0);
|
|
}
|
|
|
|
.notif-empty {
|
|
padding: 2rem 0.75rem;
|
|
text-align: center;
|
|
font-size: 0.75rem;
|
|
color: var(--ctp-overlay0);
|
|
font-style: italic;
|
|
}
|
|
</style>
|