generated from nhcarrigan/template
2db858080d
- Add font_size config field (10-24px, default 14px) - Add keyboard shortcuts: Ctrl++/- to adjust, Ctrl+0 to reset - Add font size slider in Settings > Appearance - Apply font size to Terminal and InputBar via CSS variable - Persist font size preference between sessions Closes #19
232 lines
6.3 KiB
Svelte
232 lines
6.3 KiB
Svelte
<script lang="ts">
|
|
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
|
|
import { afterUpdate, tick } from "svelte";
|
|
import ConversationTabs from "./ConversationTabs.svelte";
|
|
import Markdown from "./Markdown.svelte";
|
|
import HighlightedText from "./HighlightedText.svelte";
|
|
import { searchState, searchQuery } from "$lib/stores/search";
|
|
|
|
let terminalElement: HTMLDivElement;
|
|
let shouldAutoScroll = true;
|
|
let lines: TerminalLine[] = [];
|
|
let currentSearchQuery = "";
|
|
let currentConversationId: string | null = null;
|
|
let isRestoringScroll = false;
|
|
|
|
searchQuery.subscribe((value) => {
|
|
currentSearchQuery = value;
|
|
});
|
|
|
|
claudeStore.terminalLines.subscribe((value) => {
|
|
lines = value;
|
|
});
|
|
|
|
claudeStore.activeConversationId.subscribe(async (newId) => {
|
|
if (!newId) return;
|
|
|
|
// Save current conversation's scroll position before switching
|
|
if (currentConversationId && currentConversationId !== newId && terminalElement) {
|
|
const position = shouldAutoScroll ? -1 : terminalElement.scrollTop;
|
|
claudeStore.saveScrollPosition(currentConversationId, position);
|
|
}
|
|
|
|
currentConversationId = newId;
|
|
|
|
// Restore scroll position for the new conversation after DOM updates
|
|
await tick();
|
|
if (terminalElement) {
|
|
const savedPosition = claudeStore.getScrollPosition(newId);
|
|
isRestoringScroll = true;
|
|
if (savedPosition === -1) {
|
|
// Auto-scroll to bottom
|
|
shouldAutoScroll = true;
|
|
terminalElement.scrollTop = terminalElement.scrollHeight;
|
|
} else {
|
|
// Restore to saved position
|
|
shouldAutoScroll = false;
|
|
terminalElement.scrollTop = savedPosition;
|
|
}
|
|
// Small delay to prevent the scroll handler from overriding our restore
|
|
setTimeout(() => {
|
|
isRestoringScroll = false;
|
|
}, 50);
|
|
}
|
|
});
|
|
|
|
function handleScroll() {
|
|
if (!terminalElement || isRestoringScroll) return;
|
|
const { scrollTop, scrollHeight, clientHeight } = terminalElement;
|
|
shouldAutoScroll = scrollHeight - scrollTop - clientHeight < 100;
|
|
}
|
|
|
|
afterUpdate(() => {
|
|
if (shouldAutoScroll && terminalElement && !isRestoringScroll) {
|
|
terminalElement.scrollTop = terminalElement.scrollHeight;
|
|
}
|
|
});
|
|
|
|
function getLineClass(type: string): string {
|
|
switch (type) {
|
|
case "user":
|
|
return "terminal-user";
|
|
case "assistant":
|
|
return "terminal-assistant";
|
|
case "system":
|
|
return "terminal-system italic";
|
|
case "tool":
|
|
return "terminal-tool";
|
|
case "error":
|
|
return "terminal-error";
|
|
default:
|
|
return "terminal-default";
|
|
}
|
|
}
|
|
|
|
function getLinePrefix(type: string): string {
|
|
switch (type) {
|
|
case "user":
|
|
return ">";
|
|
case "assistant":
|
|
return "";
|
|
case "system":
|
|
return "[system]";
|
|
case "tool":
|
|
return "[tool]";
|
|
case "error":
|
|
return "[error]";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function formatTime(date: Date): string {
|
|
return date.toLocaleTimeString("en-US", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
$: {
|
|
if (currentSearchQuery && lines.length > 0) {
|
|
const escapedQuery = currentSearchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const regex = new RegExp(escapedQuery, "gi");
|
|
let totalMatches = 0;
|
|
for (const line of lines) {
|
|
const matches = line.content.match(regex);
|
|
if (matches) {
|
|
totalMatches += matches.length;
|
|
}
|
|
}
|
|
searchState.setMatchCount(totalMatches);
|
|
} else {
|
|
searchState.setMatchCount(0);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
class="terminal-container flex-1 overflow-hidden rounded-lg bg-[var(--bg-terminal)] border border-[var(--border-color)]"
|
|
>
|
|
<div
|
|
class="terminal-header flex items-center gap-2 px-4 py-2 border-b border-[var(--border-color)] bg-[var(--bg-secondary)]"
|
|
>
|
|
<div class="flex gap-1.5">
|
|
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
|
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
|
|
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
|
</div>
|
|
<span class="text-sm terminal-header-text ml-2">Terminal</span>
|
|
</div>
|
|
|
|
<ConversationTabs />
|
|
|
|
<div
|
|
bind:this={terminalElement}
|
|
onscroll={handleScroll}
|
|
class="terminal-content h-[calc(100%-76px)] overflow-y-auto p-4 font-mono"
|
|
style="font-size: var(--terminal-font-size, 14px);"
|
|
>
|
|
{#if lines.length === 0}
|
|
<div class="terminal-waiting italic">
|
|
Waiting for Claude... Type a message below to start!
|
|
</div>
|
|
{:else}
|
|
{#each lines as line (line.id)}
|
|
<div class="terminal-line mb-2 {getLineClass(line.type)}">
|
|
<span class="terminal-timestamp text-xs mr-2">{formatTime(line.timestamp)}</span>
|
|
{#if getLinePrefix(line.type)}
|
|
<span class="terminal-prefix mr-2">{getLinePrefix(line.type)}</span>
|
|
{/if}
|
|
{#if line.toolName}
|
|
<span class="terminal-tool-name mr-2">[{line.toolName}]</span>
|
|
{/if}
|
|
{#if line.type === "assistant"}
|
|
<Markdown content={line.content} searchQuery={currentSearchQuery} />
|
|
{:else}
|
|
<HighlightedText content={line.content} searchQuery={currentSearchQuery} />
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.terminal-content {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--border-color) var(--bg-terminal);
|
|
}
|
|
|
|
/* Terminal text colors that adapt to theme */
|
|
.terminal-user {
|
|
color: var(--terminal-user, #22d3ee);
|
|
}
|
|
|
|
.terminal-assistant {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.terminal-system {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.terminal-tool {
|
|
color: var(--terminal-tool, #c084fc);
|
|
}
|
|
|
|
.terminal-error {
|
|
color: var(--terminal-error, #f87171);
|
|
}
|
|
|
|
.terminal-default {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.terminal-timestamp {
|
|
color: var(--text-tertiary, #6b7280);
|
|
}
|
|
|
|
.terminal-prefix {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.terminal-tool-name {
|
|
color: var(--terminal-tool-name, #ddd6fe);
|
|
}
|
|
|
|
.terminal-waiting {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.terminal-header-text {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
:global(.search-highlight) {
|
|
background-color: var(--search-highlight, #fbbf24);
|
|
color: var(--search-highlight-text, #000);
|
|
border-radius: 2px;
|
|
padding: 0 2px;
|
|
}
|
|
</style>
|