2026-04-19 19:39:04 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { ChatMode, iconForChatMode } from "Enums/ChatMode";
|
|
|
|
|
import { Copy } from "Enums/Copy";
|
2026-05-25 17:22:07 +00:00
|
|
|
import { onDestroy, tick } from "svelte";
|
2026-04-19 19:39:04 +00:00
|
|
|
import { setIcon } from "obsidian";
|
2026-05-05 19:54:34 +00:00
|
|
|
import type { SettingsService } from "Services/SettingsService";
|
|
|
|
|
import { Resolve } from "Services/DependencyService";
|
|
|
|
|
import { Services } from "Services/Services";
|
2026-04-19 19:39:04 +00:00
|
|
|
|
|
|
|
|
export let focusInput: () => void;
|
|
|
|
|
export let chatModeSelectionAreaActive: boolean;
|
2026-05-05 19:54:34 +00:00
|
|
|
|
|
|
|
|
const settingsService: SettingsService = Resolve<SettingsService>(Services.SettingsService);
|
2026-05-25 17:22:07 +00:00
|
|
|
|
|
|
|
|
const settingsSubscription: object = settingsService.subscribeToSettingsChanged(() => currentChatMode = settingsService.settings.chatMode);
|
|
|
|
|
|
|
|
|
|
onDestroy(() => settingsService.unsubscribe(settingsSubscription));
|
2026-04-19 19:39:04 +00:00
|
|
|
|
|
|
|
|
let height = 0;
|
|
|
|
|
|
|
|
|
|
let ChatModeSelectionContentDiv: HTMLDivElement;
|
|
|
|
|
let chatModeSelectionContainer: HTMLDivElement;
|
|
|
|
|
|
2026-05-05 19:54:34 +00:00
|
|
|
let currentChatMode: ChatMode = settingsService.settings.chatMode;
|
|
|
|
|
let selectedChatMode: ChatMode = ChatMode.ReadOnly;
|
2026-04-19 19:39:04 +00:00
|
|
|
|
|
|
|
|
let iconElements: (HTMLDivElement | null)[] = [null, null, null];
|
2026-05-05 19:54:34 +00:00
|
|
|
let indexedModes: ChatMode[] = [ChatMode.ReadOnly, ChatMode.Edit, ChatMode.Planning];
|
2026-04-19 19:39:04 +00:00
|
|
|
|
|
|
|
|
$: if (iconElements.some(element => element !== null)) {
|
|
|
|
|
iconElements.forEach((element, index) => {
|
|
|
|
|
if (element) {
|
2026-05-05 19:54:34 +00:00
|
|
|
setIcon(element, iconForChatMode(indexedModes[index]));
|
2026-04-19 19:39:04 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: chatModeSelectionAreaActive, updateHeight();
|
|
|
|
|
|
|
|
|
|
$: if (chatModeSelectionAreaActive) {
|
|
|
|
|
selectedChatMode = ChatMode.ReadOnly;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (chatModeSelectionContainer && chatModeSelectionAreaActive) {
|
|
|
|
|
document.addEventListener("click", handleClickOutside);
|
|
|
|
|
updateHeight();
|
|
|
|
|
|
|
|
|
|
if (ChatModeSelectionContentDiv) {
|
|
|
|
|
ChatModeSelectionContentDiv.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 10);
|
|
|
|
|
} else {
|
|
|
|
|
document.removeEventListener("click", handleClickOutside);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateHeight() {
|
|
|
|
|
tick().then(() => {
|
|
|
|
|
if (ChatModeSelectionContentDiv) {
|
|
|
|
|
height = ChatModeSelectionContentDiv.scrollHeight;
|
|
|
|
|
} else {
|
|
|
|
|
height = 0;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleClickOutside(event: MouseEvent) {
|
|
|
|
|
if (chatModeSelectionContainer && !chatModeSelectionContainer.contains(event.target as Node)) {
|
|
|
|
|
chatModeSelectionAreaActive = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 19:54:34 +00:00
|
|
|
async function handleChatModeSelect(e?: MouseEvent) {
|
2026-04-19 19:39:04 +00:00
|
|
|
currentChatMode = selectedChatMode;
|
|
|
|
|
chatModeSelectionAreaActive = false;
|
|
|
|
|
|
2026-05-05 19:54:34 +00:00
|
|
|
await settingsService.updateSettings(settings => {
|
|
|
|
|
settings.chatMode = currentChatMode
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-19 19:39:04 +00:00
|
|
|
e?.preventDefault();
|
|
|
|
|
focusInput();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleKeydown(e: KeyboardEvent) {
|
|
|
|
|
if (!chatModeSelectionAreaActive) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
if (e.key.startsWith("Arrow")) {
|
2026-05-05 19:54:34 +00:00
|
|
|
const index = indexedModes.indexOf(selectedChatMode);
|
2026-04-19 19:39:04 +00:00
|
|
|
if (e.key === "ArrowUp") {
|
2026-05-05 19:54:34 +00:00
|
|
|
selectedChatMode = index === 0 ? indexedModes[2] : indexedModes[index - 1];
|
2026-04-19 19:39:04 +00:00
|
|
|
}
|
|
|
|
|
if (e.key === "ArrowDown") {
|
2026-05-05 19:54:34 +00:00
|
|
|
selectedChatMode = index >= 2 ? indexedModes[0] : indexedModes[index + 1];
|
2026-04-19 19:39:04 +00:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
handleChatModeSelect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
chatModeSelectionAreaActive = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div id="chat-mode-selection" style:height="{height}px" bind:this={chatModeSelectionContainer}>
|
|
|
|
|
{#if chatModeSelectionAreaActive}
|
|
|
|
|
<div
|
|
|
|
|
id="chat-mode-selection-inner-container"
|
|
|
|
|
bind:this={ChatModeSelectionContentDiv}
|
|
|
|
|
role="listbox"
|
|
|
|
|
tabindex="0"
|
|
|
|
|
on:keydown={handleKeydown}>
|
|
|
|
|
<div class="chat-mode-selection-container"
|
|
|
|
|
role="option"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
aria-selected={selectedChatMode === ChatMode.ReadOnly}
|
|
|
|
|
class:current-chat-mode={currentChatMode === ChatMode.ReadOnly}
|
|
|
|
|
style:background-color={selectedChatMode === ChatMode.ReadOnly ? "var(--interactive-accent)" : "transparent"}
|
|
|
|
|
on:mouseenter={() => selectedChatMode = ChatMode.ReadOnly}
|
|
|
|
|
on:mousedown={handleChatModeSelect}
|
|
|
|
|
on:keydown={() => {}}>
|
2026-05-05 19:54:34 +00:00
|
|
|
<div class="chat-mode-selection-icon" bind:this={iconElements[indexedModes.indexOf(ChatMode.ReadOnly)]}></div>
|
2026-04-19 19:39:04 +00:00
|
|
|
<div class="chat-mode-selection-title">{Copy.ChatModeReadOnlyTitle}</div>
|
|
|
|
|
<div class="chat-mode-selection-subtitle">{Copy.ChatModeReadOnlyDesc}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chat-mode-selection-container"
|
|
|
|
|
role="option"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
aria-selected={selectedChatMode === ChatMode.Edit}
|
|
|
|
|
class:current-chat-mode={currentChatMode === ChatMode.Edit}
|
|
|
|
|
style:background-color={selectedChatMode === ChatMode.Edit ? "var(--interactive-accent)" : "transparent"}
|
|
|
|
|
on:mouseenter={() => selectedChatMode = ChatMode.Edit}
|
|
|
|
|
on:mousedown={handleChatModeSelect}
|
|
|
|
|
on:keydown={() => {}}>
|
2026-05-05 19:54:34 +00:00
|
|
|
<div class="chat-mode-selection-icon" bind:this={iconElements[indexedModes.indexOf(ChatMode.Edit)]}></div>
|
2026-04-19 19:39:04 +00:00
|
|
|
<div class="chat-mode-selection-title">{Copy.ChatModeEditTitle}</div>
|
|
|
|
|
<div class="chat-mode-selection-subtitle">{Copy.ChatModeEditDesc}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chat-mode-selection-container"
|
|
|
|
|
role="option"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
aria-selected={selectedChatMode === ChatMode.Planning}
|
|
|
|
|
class:current-chat-mode={currentChatMode === ChatMode.Planning}
|
|
|
|
|
style:background-color={selectedChatMode === ChatMode.Planning ? "var(--interactive-accent)" : "transparent"}
|
|
|
|
|
on:mouseenter={() => selectedChatMode = ChatMode.Planning}
|
|
|
|
|
on:mousedown={handleChatModeSelect}
|
|
|
|
|
on:keydown={() => {}}>
|
2026-05-05 19:54:34 +00:00
|
|
|
<div class="chat-mode-selection-icon" bind:this={iconElements[indexedModes.indexOf(ChatMode.Planning)]}></div>
|
2026-04-19 19:39:04 +00:00
|
|
|
<div class="chat-mode-selection-title">{Copy.ChatModePlanningTitle}</div>
|
|
|
|
|
<div class="chat-mode-selection-subtitle">{Copy.ChatModePlanningDesc}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
#chat-mode-selection {
|
|
|
|
|
max-height: 15em;
|
|
|
|
|
transition: height 0.2s ease-out;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-mode-selection::-webkit-scrollbar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-mode-selection-inner-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--size-2-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-mode-selection-container {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-rows: auto auto;
|
|
|
|
|
grid-template-columns: auto 1fr;
|
|
|
|
|
border-style: solid;
|
|
|
|
|
border-radius: var(--size-2-2);
|
|
|
|
|
border-color: var(--background-primary-alt);
|
|
|
|
|
border-width: 1px;
|
|
|
|
|
padding: var(--size-2-2) var(--size-4-2);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-mode-selection-icon {
|
|
|
|
|
grid-row: 1 / 3;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding-right: var(--size-4-2);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-mode-selection-container.current-chat-mode {
|
|
|
|
|
box-shadow: inset 0px 0px 4px 1px var(--color-accent);
|
|
|
|
|
border-color: var(--color-accent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-mode-selection-title {
|
|
|
|
|
grid-row: 1;
|
|
|
|
|
grid-column: 2;
|
|
|
|
|
font-family: var(--font-interface-theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-mode-selection-subtitle {
|
|
|
|
|
grid-row: 2;
|
|
|
|
|
grid-column: 2;
|
|
|
|
|
font-family: var(--font-interface-theme);
|
|
|
|
|
font-size: var(--font-smallest);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:global(.is-mobile) .chat-mode-selection-container[aria-selected="true"] {
|
|
|
|
|
background-color: transparent !important;
|
|
|
|
|
}
|
|
|
|
|
</style>
|