feat: another wave of features (#61)
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled

## Explanation

This PR bundles several user-facing improvements and feature additions for the v0.3.0 release, including quality-of-life improvements to the UI, new slash commands, better state persistence, and auto-update checking.

## Included Changes

- **Resizable chat input** with drag handle (#58 partial)
- **Arrow key navigation fix** - cursor keys now navigate text when user has typed input (#58)
- **Scroll position persistence** per conversation tab
- **/skill command** for invoking Claude Code skills (#57)
- **Stats persistence fix** - stats now persist across session changes, only reset on disconnect (#59)
- **Auto-update checker** on startup (#17)
- **Resizable character panel** with full-height sprites (#10)
- **Font size and zoom settings** with keyboard shortcuts (Ctrl++/Ctrl+-/Ctrl+0) (#19)

## Closes

Closes #10, #17, #19, #57, #58, #59

## Attestations

- [x] I have read and agree to the Code of Conduct
- [x] I have read and agree to the Community Guidelines
- [x] My contribution complies with the Contributor Covenant
- [x] I have run the linter and resolved any errors
- [x] My pull request uses an appropriate title, matching the conventional commit standards
- [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request
- [x] All new and existing tests pass locally with my changes
- [x] Code coverage remains at or above the configured threshold

## Documentation

N/A - Internal app features

## Versioning

Minor - My pull request introduces new non-breaking features.

---
 This PR was created with help from Hikari~ 🌸

Co-authored-by: Hikari <hikari@nhcarrigan.com>
Reviewed-on: #61
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #61.
This commit is contained in:
2026-01-23 19:07:22 -08:00
committed by Naomi Carrigan
parent 06810537a9
commit 3f30997f0e
34 changed files with 1562 additions and 306 deletions
+134 -34
View File
@@ -38,6 +38,38 @@
let inputHistory = $state<string[]>([]);
let historyIndex = $state(-1);
let tempInput = $state("");
let userHasTyped = $state(false); // Track if user manually typed (vs history navigation)
// Textarea resize state
let textareaHeight = $state(48);
const MIN_HEIGHT = 48;
const MAX_HEIGHT = 200;
let isResizing = $state(false);
let startY = 0;
let startHeight = 0;
function handleResizeStart(event: MouseEvent) {
isResizing = true;
startY = event.clientY;
startHeight = textareaHeight;
document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
event.preventDefault();
}
function handleResizeMove(event: MouseEvent) {
if (!isResizing) return;
// Dragging up (negative deltaY) should increase height
const deltaY = startY - event.clientY;
const newHeight = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, startHeight + deltaY));
textareaHeight = newHeight;
}
function handleResizeEnd() {
isResizing = false;
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
}
// Load history from localStorage on init
function loadHistory(): string[] {
@@ -81,6 +113,13 @@
});
function handleInputChange() {
// If input is empty, allow history navigation again
// Otherwise, mark that user has manually typed
if (inputValue === "") {
userHasTyped = false;
} else {
userHasTyped = true;
}
// Reset history navigation when user types
historyIndex = -1;
tempInput = "";
@@ -125,6 +164,7 @@
addToHistory(message);
historyIndex = -1;
tempInput = "";
userHasTyped = false;
const wasCommand = await executeSlashCommand();
if (wasCommand) return;
@@ -144,6 +184,7 @@
addToHistory(message);
historyIndex = -1;
tempInput = "";
userHasTyped = false;
isSubmitting = true;
inputValue = "";
@@ -277,8 +318,9 @@ User: ${formattedMessage}`;
}
}
// Handle input history navigation (when command menu is closed)
if (event.key === "ArrowUp" && inputHistory.length > 0) {
// Handle input history navigation (when command menu is closed AND user hasn't typed)
// If user has typed something, let arrow keys navigate the cursor instead
if (event.key === "ArrowUp" && inputHistory.length > 0 && !userHasTyped) {
event.preventDefault();
if (historyIndex === -1) {
// Save current input before navigating history
@@ -291,12 +333,13 @@ User: ${formattedMessage}`;
return;
}
if (event.key === "ArrowDown" && historyIndex >= 0) {
if (event.key === "ArrowDown" && historyIndex >= 0 && !userHasTyped) {
event.preventDefault();
historyIndex--;
if (historyIndex === -1) {
// Restore the temp input when going back to current
inputValue = tempInput;
userHasTyped = false; // Reset since we're back to empty/temp state
} else {
inputValue = inputHistory[historyIndex];
}
@@ -314,13 +357,15 @@ User: ${formattedMessage}`;
<MessageModeSelector />
</div>
<div class="input-row flex gap-3 items-end">
<div class="flex-1 relative">
<div class="input-row">
<div class="textarea-wrapper">
<SlashCommandMenu
commands={matchingCommands}
selectedIndex={selectedCommandIndex}
onSelect={selectCommand}
/>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="resize-handle" onmousedown={handleResizeStart} title="Drag to resize"></div>
<textarea
bind:value={inputValue}
onkeydown={handleKeyDown}
@@ -330,41 +375,39 @@ User: ${formattedMessage}`;
: "Connect to Claude first..."}
disabled={isSubmitting}
rows={1}
style="height: {textareaHeight}px; font-size: var(--terminal-font-size, 14px);"
class="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-color)]
rounded-lg text-[var(--text-primary)] placeholder-gray-500 resize-none
focus:outline-none focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)]
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-200"
disabled:opacity-50 disabled:cursor-not-allowed"
></textarea>
</div>
{#if isProcessing}
<button
type="button"
onclick={handleInterrupt}
class="px-6 py-3 bg-red-600 hover:bg-red-700
text-white font-medium rounded-lg
transition-all duration-200 transform hover:scale-105 active:scale-95"
title="Interrupt the current response (Ctrl+C)"
>
<span class="font-bold"></span> Stop
</button>
{:else}
<button
type="submit"
disabled={!isConnected || isSubmitting || !inputValue.trim()}
class="px-6 py-3 bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
text-white font-medium rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed
transition-all duration-200 transform hover:scale-105 active:scale-95"
>
{#if isSubmitting}
<span class="inline-block animate-spin"></span>
{:else}
Send
{/if}
</button>
{/if}
<div class="button-wrapper">
{#if isProcessing}
<button
type="button"
onclick={handleInterrupt}
class="send-button bg-red-600 hover:bg-red-700"
title="Interrupt the current response (Ctrl+C)"
>
<span class="font-bold"></span> Stop
</button>
{:else}
<button
type="submit"
disabled={!isConnected || isSubmitting || !inputValue.trim()}
class="send-button bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
disabled:opacity-50 disabled:cursor-not-allowed"
>
{#if isSubmitting}
<span class="inline-block animate-spin"></span>
{:else}
Send
{/if}
</button>
{/if}
</div>
</div>
</form>
@@ -386,4 +429,61 @@ User: ${formattedMessage}`;
gap: 12px;
align-items: flex-end;
}
.textarea-wrapper {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}
.resize-handle {
height: 6px;
cursor: ns-resize;
background: transparent;
border-radius: 3px;
margin-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
}
.resize-handle::before {
content: "";
width: 40px;
height: 3px;
background: var(--border-color);
border-radius: 2px;
opacity: 0.5;
transition: opacity 0.2s;
}
.resize-handle:hover::before {
opacity: 1;
background: var(--accent-primary);
}
.button-wrapper {
display: flex;
align-items: flex-end;
height: 100%;
}
.send-button {
padding: 0 24px;
height: 48px;
color: white;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s;
white-space: nowrap;
}
.send-button:hover:not(:disabled) {
transform: scale(1.05);
}
.send-button:active:not(:disabled) {
transform: scale(0.95);
}
</style>