generated from nhcarrigan/template
b745100bd5
## Summary This PR covers the full audit of Claude CLI changes from 2.1.50 to 2.1.53, plus a batch of bug fixes, new features, and maintenance work identified during that review. ### New Features - **Workspace trust gate** — detects hooks, MCP servers, and custom commands in a workspace before connecting; persists trust decisions so users aren't prompted repeatedly - **Custom background image** — users can set a background image with configurable opacity; character panel and compact mode go transparent when active - **Draggable tab reordering** — conversation tabs can be reordered via pointer-event drag-and-drop (HTML5 drag is intercepted by Tauri/WebView2, so pointer events are used instead) - **Org UUID in account info** — exposes the org UUID from Claude auth status ### Bug Fixes - **Unread dot false positives** — initialise unread counts on mount to prevent all tabs showing the blue dot after toggling the file editor (Closes #164) - **Watchdog for hung WSL bridge** — detects connections that never receive `system:init` and kills the stale process after 1 minute (Closes #166) - **Suppress terminal window flash on Windows** — applies `CREATE_NO_WINDOW` to all subprocesses via a `HideWindow` trait extension (Closes #165) - **HTML escaping in markdown renderer** — escape `<` and `>` in `codespan` and `html` renderer callbacks to prevent raw HTML injection (Closes #169) ### Maintenance - Verify stream-JSON handles tool results above the 50K threshold correctly (Closes #162) - Reviewed hook security fixes from CLI 2.1.51 — not applicable to our setup (Closes #163) - Expose org UUID from `claude auth status` (Closes #160) - Clean up Svelte and Vite build warnings (`a11y_click_events_have_key_events`, `state_referenced_locally`, `non_reactive_update`, `codeSplitting`, chunk size, CodeMirror dynamic import) - Update all npm dependencies to latest compatible versions with exact pinning (Closes #81, Closes #82, Closes #83, Closes #84, Closes #85, Closes #86, Closes #87, Closes #90, Closes #91, Closes #93, Closes #94, Closes #95, Closes #96, Closes #97, Closes #98, Closes #99, Closes #101, Closes #141, Closes #142, Closes #143, Closes #145, Closes #146, Closes #147) - Run `cargo update` to bring Cargo.lock up to date ### Closes Closes #160 Closes #162 Closes #163 Closes #164 Closes #165 Closes #166 Closes #167 Closes #168 Closes #169 Closes #81 Closes #82 Closes #83 Closes #84 Closes #85 Closes #86 Closes #87 Closes #90 Closes #91 Closes #93 Closes #94 Closes #95 Closes #96 Closes #97 Closes #98 Closes #99 Closes #101 Closes #141 Closes #142 Closes #143 Closes #145 Closes #146 Closes #147 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #171 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
239 lines
6.5 KiB
Svelte
239 lines
6.5 KiB
Svelte
<script lang="ts">
|
|
import type { EditorView } from "@codemirror/view";
|
|
|
|
interface Props {
|
|
x: number;
|
|
y: number;
|
|
editorView: EditorView;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { x, y, editorView, onClose }: Props = $props();
|
|
|
|
// Menu element reference for measuring
|
|
let menuElement: HTMLDivElement | undefined = $state();
|
|
|
|
// Adjusted position to keep menu within viewport
|
|
let adjustedX = $derived.by(() => {
|
|
if (!menuElement) return x;
|
|
const menuWidth = menuElement.offsetWidth || 180;
|
|
const maxX = window.innerWidth - menuWidth - 8;
|
|
return Math.min(x, maxX);
|
|
});
|
|
|
|
let adjustedY = $derived.by(() => {
|
|
if (!menuElement) return y;
|
|
const menuHeight = menuElement.offsetHeight || 250;
|
|
const maxY = window.innerHeight - menuHeight - 8;
|
|
return Math.min(y, maxY);
|
|
});
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") {
|
|
onClose();
|
|
}
|
|
}
|
|
|
|
function execCommand(command: "cut" | "copy" | "paste" | "selectAll" | "undo" | "redo") {
|
|
editorView.focus();
|
|
|
|
switch (command) {
|
|
case "cut":
|
|
document.execCommand("cut");
|
|
break;
|
|
case "copy":
|
|
document.execCommand("copy");
|
|
break;
|
|
case "paste":
|
|
document.execCommand("paste");
|
|
break;
|
|
case "selectAll":
|
|
editorView.dispatch({
|
|
selection: { anchor: 0, head: editorView.state.doc.length },
|
|
});
|
|
break;
|
|
case "undo":
|
|
import("@codemirror/commands").then(({ undo }) => {
|
|
undo(editorView);
|
|
});
|
|
break;
|
|
case "redo":
|
|
import("@codemirror/commands").then(({ redo }) => {
|
|
redo(editorView);
|
|
});
|
|
break;
|
|
}
|
|
onClose();
|
|
}
|
|
|
|
// Check if there's a selection
|
|
let hasSelection = $derived(
|
|
editorView.state.selection.main.from !== editorView.state.selection.main.to
|
|
);
|
|
</script>
|
|
|
|
<svelte:window onkeydown={handleKeydown} />
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
|
<div
|
|
class="menu-overlay"
|
|
onclick={onClose}
|
|
oncontextmenu={(e) => {
|
|
e.preventDefault();
|
|
onClose();
|
|
}}
|
|
>
|
|
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
|
<div
|
|
bind:this={menuElement}
|
|
class="menu-content"
|
|
style="left: {adjustedX}px; top: {adjustedY}px;"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<button class="menu-item" onclick={() => execCommand("undo")}>
|
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M3 10h10a5 5 0 0 1 5 5v2M3 10l4-4M3 10l4 4"
|
|
/>
|
|
</svg>
|
|
Undo
|
|
<span class="shortcut">Ctrl+Z</span>
|
|
</button>
|
|
|
|
<button class="menu-item" onclick={() => execCommand("redo")}>
|
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M21 10H11a5 5 0 0 0-5 5v2M21 10l-4-4M21 10l-4 4"
|
|
/>
|
|
</svg>
|
|
Redo
|
|
<span class="shortcut">Ctrl+Y</span>
|
|
</button>
|
|
|
|
<div class="menu-divider"></div>
|
|
|
|
<button class="menu-item" onclick={() => execCommand("cut")} disabled={!hasSelection}>
|
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M14.121 14.121L19 19m-7-7l7-7m-7 7l-2.879 2.879M12 12L9.121 9.121m0 5.758a3 3 0 1 0-4.243-4.243 3 3 0 0 0 4.243 4.243zm0-5.758a3 3 0 1 0-4.243-4.243 3 3 0 0 0 4.243 4.243z"
|
|
/>
|
|
</svg>
|
|
Cut
|
|
<span class="shortcut">Ctrl+X</span>
|
|
</button>
|
|
|
|
<button class="menu-item" onclick={() => execCommand("copy")} disabled={!hasSelection}>
|
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M8 16H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2m-6 12h8a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2z"
|
|
/>
|
|
</svg>
|
|
Copy
|
|
<span class="shortcut">Ctrl+C</span>
|
|
</button>
|
|
|
|
<button class="menu-item" onclick={() => execCommand("paste")}>
|
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2"
|
|
/>
|
|
</svg>
|
|
Paste
|
|
<span class="shortcut">Ctrl+V</span>
|
|
</button>
|
|
|
|
<div class="menu-divider"></div>
|
|
|
|
<button class="menu-item" onclick={() => execCommand("selectAll")}>
|
|
<svg class="menu-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5z"
|
|
/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9h6v6H9z" />
|
|
</svg>
|
|
Select All
|
|
<span class="shortcut">Ctrl+A</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.menu-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 50;
|
|
}
|
|
|
|
.menu-content {
|
|
position: absolute;
|
|
z-index: 50;
|
|
min-width: 180px;
|
|
border-radius: 0.375rem;
|
|
border: 1px solid var(--border-color);
|
|
background-color: var(--bg-secondary);
|
|
padding: 0.25rem 0;
|
|
box-shadow:
|
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
width: 100%;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.375rem 0.75rem;
|
|
text-align: left;
|
|
font-size: 0.875rem;
|
|
color: var(--text-primary);
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
.menu-item:hover:not(:disabled) {
|
|
background-color: var(--bg-primary);
|
|
}
|
|
|
|
.menu-item:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.menu-divider {
|
|
margin: 0.25rem 0;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.menu-icon {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.shortcut {
|
|
margin-left: auto;
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|