Skip to content
Merged
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@types/turndown": "^5.0.6",
"@vitejs/plugin-react": "^4.7.0",
"cross-env": "^7.0.3",
"fake-indexeddb": "^6.2.5",
"jsdom": "^26.1.0",
"typescript": "^5.9.3",
"vite": "^6.4.2",
Expand Down
47 changes: 47 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import OutputWindow from "./components/output";
import PreferencesDialog, {
PreferencesDialogRef,
} from "./components/PreferencesDialog";
import AutoLogDialog, { AutoLogDialogRef } from "./components/AutoLogDialog";
import Sidebar, { SidebarRef } from "./components/sidebar";
import Statusbar from "./components/statusbar";
import Toolbar from "./components/toolbar";
Expand All @@ -22,6 +23,7 @@ import HostPanel from "./components/HostPanel";
import { useChannelHistory } from "./hooks/useChannelHistory";
import { FileTransferOffer, useClientEvent } from "./hooks/useClientEvent";
import type { GMCPMessageRoomInfo } from "./gmcp/Room";
import { autoLogService, createAutoLogSessionDraft } from "./logging/AutoLogService";

const WINDOW_TITLE = "Mongoose Client";

Expand Down Expand Up @@ -50,6 +52,7 @@ function App() {
const outRef = React.useRef<OutputWindow | null>(null);
const inRef = React.useRef<HTMLTextAreaElement | null>(null);
const prefsDialogRef = React.useRef<PreferencesDialogRef | null>(null);
const autoLogDialogRef = React.useRef<AutoLogDialogRef | null>(null);
const sidebarRef = React.useRef<SidebarRef | null>(null);

const clientInitialized = useRef(false);
Expand Down Expand Up @@ -122,6 +125,45 @@ function App() {
}
}, [client]);

useEffect(() => {
if (!client) {
autoLogService.configureSession(null);
return;
}

const configureAutologSession = () => {
autoLogService.configureSession(createAutoLogSessionDraft(document.title || WINDOW_TITLE));
};
const handleConnect = () => {
configureAutologSession();
autoLogService.startSession().catch((error) => {
console.error("Failed to start autolog session:", error);
});
};
const handleDisconnect = () => {
autoLogService.endSession().catch((error) => {
console.error("Failed to end autolog session:", error);
});
};

configureAutologSession();
if (client.connected) {
handleConnect();
}

client.on("connect", handleConnect);
client.on("disconnect", handleDisconnect);

return () => {
client.off("connect", handleConnect);
client.off("disconnect", handleDisconnect);
autoLogService.endSession().catch((error) => {
console.error("Failed to end autolog session during cleanup:", error);
});
autoLogService.configureSession(null);
};
}, [client]);

// Common client setup: notifications, auto-login, MIDI, haptics, keyboard handlers
useEffect(() => {
if (!client) return;
Expand Down Expand Up @@ -316,6 +358,9 @@ function App() {
if (client) {
client.shutdown();
}
autoLogService.flush().catch((error) => {
console.error("Failed to flush autolog entries before unload:", error);
});
// Best-effort checkpoint on tab close
const wasmWorker = (window as any).wasmWorker;
if (wasmWorker) {
Expand Down Expand Up @@ -362,6 +407,7 @@ function App() {
onCopyLog={copyLog}
onToggleSidebar={() => setShowSidebar(!showSidebar)}
onOpenPrefs={() => prefsDialogRef.current?.open()}
onOpenLogs={() => autoLogDialogRef.current?.open()}
showSidebar={showSidebar}
/>
</header>
Expand Down Expand Up @@ -394,6 +440,7 @@ function App() {
<Statusbar client={client} />
</footer>
<PreferencesDialog ref={prefsDialogRef} />
<AutoLogDialog ref={autoLogDialogRef} />
</div>
)}
{/* Default mode with no client yet — blank */}
Expand Down
17 changes: 16 additions & 1 deletion src/PreferencesStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export type HapticsPreferences = {
autoStopTimeout: number;
};

export type AutologgingPreferences = {
enabled: boolean;
maxBytes: number;
};

export type PrefState = {
general: GeneralPreferences;
speech: SpeechPreferences;
Expand All @@ -58,6 +63,7 @@ export type PrefState = {
keyboard: KeyboardPreferences;
midi: MidiPreferences;
haptics: HapticsPreferences;
autologging: AutologgingPreferences;
};

export enum PrefActionType {
Expand All @@ -70,6 +76,7 @@ export enum PrefActionType {
SetKeyboard = "SET_KEYBOARD",
SetMidi = "SET_MIDI",
SetHaptics = "SET_HAPTICS",
SetAutologging = "SET_AUTOLOGGING",
}

export type PrefAction =
Expand All @@ -81,7 +88,8 @@ export type PrefAction =
| { type: PrefActionType.SetEditorAccessibilityMode; data: boolean }
| { type: PrefActionType.SetKeyboard; data: KeyboardPreferences }
| { type: PrefActionType.SetMidi; data: MidiPreferences }
| { type: PrefActionType.SetHaptics; data: HapticsPreferences };
| { type: PrefActionType.SetHaptics; data: HapticsPreferences }
| { type: PrefActionType.SetAutologging; data: AutologgingPreferences };

class PreferencesStore {
private state: PrefState;
Expand Down Expand Up @@ -141,6 +149,10 @@ class PreferencesStore {
intensityCap: 1.0,
autoStopTimeout: 5,
},
autologging: {
enabled: false,
maxBytes: 100 * 1024 * 1024,
},
};
}

Expand All @@ -159,6 +171,7 @@ class PreferencesStore {
keyboard: { ...initial.keyboard, ...stored.keyboard },
midi: { ...initial.midi, ...stored.midi },
haptics: { ...initial.haptics, ...cleanHaptics },
autologging: { ...initial.autologging, ...stored.autologging },
};
}

Expand All @@ -182,6 +195,8 @@ class PreferencesStore {
return { ...state, midi: action.data };
case PrefActionType.SetHaptics:
return { ...state, haptics: action.data };
case PrefActionType.SetAutologging:
return { ...state, autologging: action.data };
default:
return state;
}
Expand Down
188 changes: 188 additions & 0 deletions src/components/AutoLogDialog.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
.autolog-dialog {
position: fixed;
inset: 5vh 4vw;
width: min(1100px, 92vw);
max-height: 90vh;
margin: auto;
padding: 0;
background: var(--color-bg-surface);
color: var(--color-text);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
z-index: 1200;
}

.autolog-dialog-header,
.autolog-dialog-toolbar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
}

.autolog-dialog-header h1 {
flex: 1;
margin: 0;
font-size: 1.25rem;
}

.autolog-dialog-toolbar {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}

.autolog-dialog-toolbar span {
flex: 1;
}

.autolog-dialog button {
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-sm);
background: var(--color-bg-hover);
color: var(--color-text);
cursor: pointer;
padding: var(--space-2) var(--space-3);
}

.autolog-dialog button:hover {
background: var(--color-bg-active);
border-color: var(--color-primary);
}

.autolog-dialog button:disabled {
cursor: not-allowed;
opacity: 0.55;
}

.autolog-dialog-error {
margin: var(--space-3) var(--space-4) 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-danger);
border-radius: var(--radius-sm);
color: var(--color-danger);
background: var(--color-danger-subtle);
}

.autolog-dialog-body {
display: grid;
grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
min-height: 0;
height: calc(90vh - 112px);
}

.autolog-session-list,
.autolog-entry-viewer {
min-height: 0;
overflow: auto;
padding: var(--space-3);
}

.autolog-session-list {
border-right: 1px solid var(--color-border);
}

.autolog-session-row {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
margin-bottom: var(--space-2);
background: var(--color-bg);
}

.autolog-session-row.selected {
border-color: var(--color-primary);
}

.autolog-session-main {
display: block;
width: 100%;
text-align: left;
background: transparent !important;
border: 0 !important;
}

.autolog-session-title {
display: block;
font-weight: 600;
margin-bottom: var(--space-1);
}

.autolog-session-meta,
.autolog-entry-meta {
display: block;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
}

.autolog-session-actions {
display: flex;
gap: var(--space-1);
padding: 0 var(--space-2) var(--space-2);
}

.autolog-session-actions button {
flex: 1;
padding: var(--space-1) var(--space-2);
font-size: var(--font-size-xs);
}

.autolog-entry-viewer h2 {
margin: 0 0 var(--space-1);
font-size: 1rem;
}

.autolog-entry-list {
margin-top: var(--space-3);
}

.autolog-entry {
margin: 0;
padding: var(--space-2);
border-top: 1px solid var(--color-border);
white-space: pre-wrap;
overflow-wrap: anywhere;
font-family: var(--font-family-mono);
font-size: var(--font-size-sm);
}

.autolog-entry-time {
display: block;
color: var(--color-text-tertiary);
font-size: var(--font-size-xs);
margin-bottom: var(--space-1);
}

.autolog-entry-systemInfo {
color: var(--color-info);
}

.autolog-entry-errorMessage {
color: var(--color-danger);
}

.autolog-entry-command {
color: var(--color-text-secondary);
}

.autolog-empty {
color: var(--color-text-secondary);
}

@media (max-width: 768px) {
.autolog-dialog {
inset: 2vh 2vw;
width: 96vw;
}

.autolog-dialog-body {
grid-template-columns: 1fr;
height: calc(96vh - 112px);
}

.autolog-session-list {
max-height: 34vh;
border-right: 0;
border-bottom: 1px solid var(--color-border);
}
}
Loading
Loading