Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions frontend/src/components/HealthPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Appears at the bottom of the Home/Ground view when issues exist.
*/

import React, { useCallback } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import { useSession } from "../contexts/SessionContext";
import { useWebSocket } from "../hooks/useWebSocket";
import type { HealthIssue } from "@memory-loop/shared";
Expand Down Expand Up @@ -54,8 +54,30 @@ function getCategoryLabel(category: HealthIssue["category"]): string {
* - Touch-friendly with 44px tap targets
*/
export function HealthPanel(): React.ReactNode {
const { health, toggleHealthExpanded, dismissHealthIssue } = useSession();
const { sendMessage } = useWebSocket();
const { health, vault, toggleHealthExpanded, dismissHealthIssue } = useSession();
const hasSentVaultSelectionRef = useRef(false);

// Reset vault selection tracking on reconnect
const handleReconnect = useCallback(() => {
hasSentVaultSelectionRef.current = false;
}, []);

const { sendMessage, connectionStatus } = useWebSocket({
onReconnect: handleReconnect,
});

// Send vault selection when WebSocket connects.
// Each WebSocket connection has its own server-side state.
useEffect(() => {
if (
connectionStatus === "connected" &&
vault &&
!hasSentVaultSelectionRef.current
) {
sendMessage({ type: "select_vault", vaultId: vault.id });
hasSentVaultSelectionRef.current = true;
}
}, [connectionStatus, vault, sendMessage]);

const handleToggle = useCallback(() => {
toggleHealthExpanded();
Expand Down
28 changes: 25 additions & 3 deletions frontend/src/components/MoveDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Displays a mini file tree for destination selection.
*/

import { useState, useCallback, useMemo, useEffect } from "react";
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
import { useSession } from "../contexts/SessionContext.js";
import { useWebSocket } from "../hooks/useWebSocket.js";
import type { FileEntry } from "@memory-loop/shared";
Expand Down Expand Up @@ -48,10 +48,32 @@ export function MoveDialog({
onConfirm,
onCancel,
}: MoveDialogProps): React.ReactNode {
const { browser } = useSession();
const { sendMessage } = useWebSocket();
const { browser, vault } = useSession();
const hasSentVaultSelectionRef = useRef(false);

// Reset vault selection tracking on reconnect
const handleReconnect = useCallback(() => {
hasSentVaultSelectionRef.current = false;
}, []);

const { sendMessage, connectionStatus } = useWebSocket({
onReconnect: handleReconnect,
});
const { directoryCache } = browser;

// Send vault selection when WebSocket connects.
// Each WebSocket connection has its own server-side state.
useEffect(() => {
if (
connectionStatus === "connected" &&
vault &&
!hasSentVaultSelectionRef.current
) {
sendMessage({ type: "select_vault", vaultId: vault.id });
hasSentVaultSelectionRef.current = true;
}
}, [connectionStatus, vault, sendMessage]);

// Track the selected destination directory
const [selectedDir, setSelectedDir] = useState<string>("");
// Track expanded directories in the mini tree
Expand Down
40 changes: 34 additions & 6 deletions frontend/src/components/NoteCapture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* - Meeting: Captures go to meeting-specific file
*/

import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { useWebSocket } from "../hooks/useWebSocket";
import { useSession } from "../contexts/SessionContext";
import "./NoteCapture.css";
Expand Down Expand Up @@ -60,10 +60,32 @@ export function NoteCapture({ onCaptured }: NoteCaptureProps): React.ReactNode {
const meetingTitleRef = useRef<HTMLInputElement>(null);
const retryCountRef = useRef(0);
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hasSentVaultSelectionRef = useRef(false);

const { vault, meeting, clearMeeting, setDiscussionPrefill, setMode } = useSession();
const { vault, meeting, setMeetingState, clearMeeting, setDiscussionPrefill, setMode } = useSession();

const { sendMessage, lastMessage, connectionStatus } = useWebSocket();
// Reset vault selection tracking on reconnect so we re-send select_vault
const handleReconnect = useCallback(() => {
hasSentVaultSelectionRef.current = false;
}, []);

const { sendMessage, lastMessage, connectionStatus } = useWebSocket({
onReconnect: handleReconnect,
});

// Send vault selection when WebSocket connects (initial or reconnect).
// Each WebSocket connection has its own server-side state, so we must
// send select_vault on this connection before sending capture_note.
useEffect(() => {
if (
connectionStatus === "connected" &&
vault &&
!hasSentVaultSelectionRef.current
) {
sendMessage({ type: "select_vault", vaultId: vault.id });
hasSentVaultSelectionRef.current = true;
}
}, [connectionStatus, vault, sendMessage]);

// Detect touch-only devices (no hover capability)
// On touch devices, Enter adds newlines; send button is the only way to submit
Expand Down Expand Up @@ -125,19 +147,25 @@ export function NoteCapture({ onCaptured }: NoteCaptureProps): React.ReactNode {
}
}, [lastMessage, isSubmitting]);

// Handle meeting_started response (local UI updates only; session context
// is updated by useServerMessageHandler in MainContent)
// Handle meeting_started response
useEffect(() => {
if (lastMessage?.type === "meeting_started" && isStartingMeeting) {
setIsStartingMeeting(false);
setShowMeetingPrompt(false);
setMeetingTitle("");
// Update session context with meeting state (this connection receives the response)
setMeetingState({
isActive: true,
title: lastMessage.title,
filePath: lastMessage.filePath,
startedAt: lastMessage.startedAt,
});
showToast("success", `Meeting started: ${lastMessage.title}`);
requestAnimationFrame(() => {
textareaRef.current?.focus();
});
}
}, [lastMessage, isStartingMeeting]);
}, [lastMessage, isStartingMeeting, setMeetingState]);

// Handle meeting_stopped response
useEffect(() => {
Expand Down