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>
This commit is contained in:
2026-03-07 11:34:06 -05:00
parent a8bb687349
commit 74ebb8a87f
15 changed files with 1927 additions and 9 deletions
+13
View File
@@ -124,6 +124,19 @@ async def change_password(user_id: int, current_password: str, new_password: str
return user.session_version
async def invalidate_other_sessions(user_id: int) -> int:
"""Bump session_version so all sessions except the current one are invalidated.
Returns the new session_version so the caller can update the active session cookie.
"""
async with async_session() as session:
user = await session.get(User, user_id)
user.session_version += 1
await session.commit()
logger.info("Sessions invalidated for user %d (%s)", user_id, user.username)
return user.session_version
async def is_registration_open() -> bool:
"""Check if new user registration is allowed.
@@ -150,6 +150,7 @@ async def run_generation(
excluded_note_ids: list[int] | None = None,
think: bool = False,
rag_project_id: int | None = None,
workspace_project_id: int | None = None,
) -> None:
"""Stream LLM response into buffer with periodic DB flushes."""
MAX_TOOL_ROUNDS = 5
@@ -181,6 +182,7 @@ async def run_generation(
include_note_ids=include_note_ids,
excluded_note_ids=excluded_note_ids,
rag_project_id=rag_project_id,
workspace_project_id=workspace_project_id,
))
messages, context_meta = await context_task
+17
View File
@@ -451,6 +451,7 @@ async def build_context(
include_note_ids: list[int] | None = None,
excluded_note_ids: list[int] | None = None,
rag_project_id: int | None = None,
workspace_project_id: int | None = None,
) -> tuple[list[dict], dict]:
"""Build messages array for Ollama with system prompt and context.
@@ -637,6 +638,22 @@ async def build_context(
f"\n\n--- Content from {url} ---\n{content}\n--- End URL Content ---"
)
# Inject workspace context when user is in a project workspace
if workspace_project_id is not None:
from fabledassistant.services.projects import get_project
try:
wp = await get_project(user_id, workspace_project_id)
if wp:
system_parts.append(
f"\n\n--- Active Workspace ---\n"
f"You are in the \"{wp.title}\" project workspace.\n"
f"All notes and tasks you create or update MUST belong to this project.\n"
f"Always pass project=\"{wp.title}\" when calling create_note or create_task.\n"
f"--- End Active Workspace ---"
)
except Exception:
logger.warning("Failed to fetch workspace project %d", workspace_project_id)
# Inject compressed summary of older exchanges when history has been trimmed
if history_summary:
system_parts.append(