Files
FabledGrimoire/frontend/src/App.vue
T
bvandeusenandClaude Sonnet 4.6 74ebb8a87f Project Workspace view, abort button, session invalidation, workspace fixes
Workspace (/workspace/:projectId):
- Three-panel layout (tasks / chat / notes) with CSS grid collapse toggles
- WorkspaceTaskPanel: tasks grouped by milestone, collapsible groups, task
  detail slide-over with status cycling, TaskLogSection work log, and
  inline-confirm delete
- WorkspaceNoteEditor: list view (sorted by updated_at, inline tag pills,
  inline-confirm delete) with editor view (TipTap, TagInput, Suggest tags,
  60s autosave)
- Persistent workspace conversation stored in localStorage per project;
  reused on return visits
- Thinking enabled (think: true) with Reasoning block in streaming bubble
- workspace_project_id backend pipeline: chat.py → generation_task.py →
  llm.py; system prompt uses project title so agent passes project="Title"
  to tools (fixes create_note failing with numeric project string)
- SSE tool-call watcher bridges agent actions to panel updates
- Height fix: workspace-root uses height 100%; app-content switches to
  overflow hidden via :has() selector
- Entry point: "Open Workspace" button on ProjectView

Abort button:
- Stop button in ChatView header and WorkspaceView input bar
- Calls existing cancelGeneration() / POST .../generation/cancel

Session invalidation:
- POST /api/auth/invalidate-sessions bumps session_version, keeps current
  session alive; useful after SSO/OAuth password rotation
- Button in Settings → Active Sessions section

Other:
- Dashboard recent notes limit increased from 8 to 16
- Workspace chat abort replaces Send button while streaming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:34:06 -05:00

383 lines
11 KiB
Vue

<script setup lang="ts">
import { onMounted, onUnmounted, watch } from "vue";
import { useRouter } from "vue-router";
import AppHeader from "@/components/AppHeader.vue";
import ToastNotification from "@/components/ToastNotification.vue";
import { useTheme } from "@/composables/useTheme";
import { useShortcuts } from "@/composables/useShortcuts";
import { useAuthStore } from "@/stores/auth";
import { useChatStore } from "@/stores/chat";
import { useSettingsStore } from "@/stores/settings";
useTheme();
const router = useRouter();
const authStore = useAuthStore();
const chatStore = useChatStore();
const settingsStore = useSettingsStore();
const { showShortcuts, toggleShortcuts, closeShortcuts } = useShortcuts();
function startAppServices() {
chatStore.startStatusPolling();
settingsStore.fetchSettings();
}
function stopAppServices() {
chatStore.stopStatusPolling();
}
function isInputActive(): boolean {
const el = document.activeElement;
if (!el) return false;
const tag = (el as HTMLElement).tagName;
return tag === "INPUT" || tag === "TEXTAREA" || (el as HTMLElement).isContentEditable;
}
// Sequence shortcut support (e.g. g+n, g+t)
let pendingPrefix: string | null = null;
let prefixTimeout: ReturnType<typeof setTimeout> | null = null;
function clearPrefix() {
pendingPrefix = null;
if (prefixTimeout) clearTimeout(prefixTimeout);
prefixTimeout = null;
}
function onGlobalKeydown(e: KeyboardEvent) {
if (!authStore.isAuthenticated) return;
// ? — toggle shortcuts overlay (only when not typing)
if (e.key === "?" && !isInputActive() && !e.ctrlKey && !e.metaKey) {
toggleShortcuts();
return;
}
// Escape — progressive: close overlay → unfocus field → go home
if (e.key === "Escape") {
clearPrefix();
if (showShortcuts.value) {
closeShortcuts();
return;
}
if (isInputActive()) {
(document.activeElement as HTMLElement).blur();
return;
}
router.push("/");
return;
}
// All remaining shortcuts: ignore when typing or using modifier keys
if (isInputActive() || e.ctrlKey || e.metaKey || e.altKey) return;
// Complete a g+X sequence
if (pendingPrefix === "g") {
clearPrefix();
switch (e.key) {
case "h": router.push("/"); break;
case "n": router.push("/notes"); break;
case "t": router.push("/tasks"); break;
case "p": router.push("/projects"); break;
case "c": router.push("/chat"); break;
case "g": router.push("/graph"); break;
}
return;
}
// Single-key shortcuts
switch (e.key) {
case "g":
pendingPrefix = "g";
prefixTimeout = setTimeout(clearPrefix, 1500);
break;
case "n":
router.push("/notes/new");
break;
case "t":
router.push("/tasks/new");
break;
case "e": {
const name = router.currentRoute.value.name;
if (name === "note-view") {
router.push(router.currentRoute.value.path + "/edit");
}
break;
}
case "/":
e.preventDefault();
document.dispatchEvent(new CustomEvent("shortcut:focus-search"));
break;
case "c":
if (router.currentRoute.value.name === "home") {
document.dispatchEvent(new CustomEvent("shortcut:focus-chat"));
} else {
router.push("/chat");
}
break;
}
}
onMounted(async () => {
document.addEventListener("keydown", onGlobalKeydown);
await authStore.checkAuth();
if (authStore.isAuthenticated) {
startAppServices();
}
});
watch(
() => authStore.isAuthenticated,
(authenticated) => {
if (authenticated) {
startAppServices();
} else {
stopAppServices();
}
}
);
onUnmounted(() => {
document.removeEventListener("keydown", onGlobalKeydown);
stopAppServices();
});
</script>
<template>
<template v-if="authStore.isAuthenticated">
<div class="app-shell">
<AppHeader />
<div class="app-content">
<router-view />
</div>
</div>
<!-- Keyboard shortcuts overlay -->
<Transition name="shortcuts-fade">
<div v-if="showShortcuts" class="shortcuts-overlay" @click.self="closeShortcuts">
<div class="shortcuts-panel">
<div class="shortcuts-header">
<h3>Keyboard Shortcuts</h3>
<button class="shortcuts-close" @click="closeShortcuts">&times;</button>
</div>
<div class="shortcuts-body">
<div class="shortcuts-section">
<div class="shortcuts-section-title">Navigation</div>
<div class="shortcut-row">
<kbd class="shortcut-key">g</kbd>
<span class="shortcut-key-sep">+</span>
<kbd class="shortcut-key">h</kbd>
<span class="shortcut-desc">Home</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">g</kbd>
<span class="shortcut-key-sep">+</span>
<kbd class="shortcut-key">n</kbd>
<span class="shortcut-desc">Notes</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">g</kbd>
<span class="shortcut-key-sep">+</span>
<kbd class="shortcut-key">t</kbd>
<span class="shortcut-desc">Tasks</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">g</kbd>
<span class="shortcut-key-sep">+</span>
<kbd class="shortcut-key">p</kbd>
<span class="shortcut-desc">Projects</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">g</kbd>
<span class="shortcut-key-sep">+</span>
<kbd class="shortcut-key">c</kbd>
<span class="shortcut-desc">Chat</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">Esc</kbd>
<span class="shortcut-desc">Unfocus field → go home</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">?</kbd>
<span class="shortcut-desc">Toggle this panel</span>
</div>
</div>
<div class="shortcuts-section">
<div class="shortcuts-section-title">Create</div>
<div class="shortcut-row">
<kbd class="shortcut-key">n</kbd>
<span class="shortcut-desc">New note</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">t</kbd>
<span class="shortcut-desc">New task</span>
</div>
</div>
<div class="shortcuts-section">
<div class="shortcuts-section-title">Lists</div>
<div class="shortcut-row">
<kbd class="shortcut-key">/</kbd>
<span class="shortcut-desc">Focus search bar</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">j</kbd>
<span class="shortcut-key-sep">/</span>
<kbd class="shortcut-key">k</kbd>
<span class="shortcut-desc">Move down / up</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">Enter</kbd>
<span class="shortcut-desc">Open selected item</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">e</kbd>
<span class="shortcut-desc">Edit current item (viewer)</span>
</div>
</div>
<div class="shortcuts-section">
<div class="shortcuts-section-title">Chat</div>
<div class="shortcut-row">
<kbd class="shortcut-key">c</kbd>
<span class="shortcut-desc">Focus chat (home) / go to chat</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">Enter</kbd>
<span class="shortcut-desc">Send message</span>
</div>
<div class="shortcut-row">
<kbd class="shortcut-key">Shift</kbd>
<span class="shortcut-key-sep">+</span>
<kbd class="shortcut-key">Enter</kbd>
<span class="shortcut-desc">New line</span>
</div>
</div>
</div>
</div>
</div>
</Transition>
</template>
<template v-else>
<router-view />
</template>
<ToastNotification />
</template>
<style>
.app-shell {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
.app-shell > .app-header {
flex-shrink: 0;
}
.app-content {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.app-content:has(.workspace-root) {
overflow: hidden;
}
/* Shortcuts overlay */
.shortcuts-overlay {
position: fixed;
inset: 0;
background: var(--color-overlay, rgba(0, 0, 0, 0.45));
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
}
.shortcuts-panel {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md, 8px);
box-shadow: 0 8px 32px var(--color-shadow, rgba(0,0,0,0.2));
width: min(420px, 92vw);
overflow: hidden;
}
.shortcuts-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1rem 0.75rem;
border-bottom: 1px solid var(--color-border);
}
.shortcuts-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
}
.shortcuts-close {
background: none;
border: none;
font-size: 1.4rem;
line-height: 1;
color: var(--color-text-muted);
cursor: pointer;
padding: 0 0.25rem;
}
.shortcuts-close:hover {
color: var(--color-text);
}
.shortcuts-body {
padding: 0.75rem 1rem 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.shortcuts-section-title {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-muted);
margin-bottom: 0.4rem;
}
.shortcut-row {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0;
}
.shortcut-key {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.8rem;
padding: 0.15rem 0.4rem;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-bottom-width: 2px;
border-radius: 4px;
font-size: 0.78rem;
font-family: ui-monospace, monospace;
color: var(--color-text);
white-space: nowrap;
user-select: none;
}
.shortcut-key-sep {
font-size: 0.78rem;
color: var(--color-text-muted);
}
.shortcut-desc {
font-size: 0.875rem;
color: var(--color-text);
margin-left: 0.25rem;
}
/* Transition */
.shortcuts-fade-enter-active,
.shortcuts-fade-leave-active {
transition: opacity 0.15s ease;
}
.shortcuts-fade-enter-from,
.shortcuts-fade-leave-to {
opacity: 0;
}
</style>