Skip to content
Open
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
3,672 changes: 3,672 additions & 0 deletions src-tauri/src/commands/chat.rs

Large diffs are not rendered by default.

726 changes: 726 additions & 0 deletions src-tauri/src/commands/settings.rs

Large diffs are not rendered by default.

862 changes: 862 additions & 0 deletions src-tauri/src/inference/client.rs

Large diffs are not rendered by default.

438 changes: 438 additions & 0 deletions src-tauri/src/inference/types.rs

Large diffs are not rendered by default.

897 changes: 897 additions & 0 deletions src-tauri/src/lib.rs

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useEffect } from "react";

import { ChatPanel } from "./components/Chat";
import { FileBrowser } from "./components/FileBrowser";
import { OnboardingWizard } from "./components/Onboarding";
import { SettingsPanel } from "./components/Settings";
import { useOnboardingStore } from "./stores/onboardingStore";
import { useSettingsStore } from "./stores/settingsStore";

/**
* Root application component.
*
* Shows the OnboardingWizard on first run, then the main app layout.
*/
export function App(): React.JSX.Element {
const toggleSettings = useSettingsStore((s) => s.togglePanel);
const isSettingsOpen = useSettingsStore((s) => s.isOpen);
const startConfigWatch = useSettingsStore((s) => s.startConfigWatch);
const stopConfigWatch = useSettingsStore((s) => s.stopConfigWatch);
const configReloadNotification = useSettingsStore(
(s) => s.configReloadNotification,
);
const clearConfigReloadNotification = useSettingsStore(
(s) => s.clearConfigReloadNotification,
);
Comment on lines +1 to +25
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports point to modules that don't exist under src/ in this PR (e.g. ./components/Chat, ./components/FileBrowser, ./stores/onboardingStore). This will break the frontend build unless the missing modules are added or the imports are corrected to the actual locations.

Suggested change
import { useEffect } from "react";
import { ChatPanel } from "./components/Chat";
import { FileBrowser } from "./components/FileBrowser";
import { OnboardingWizard } from "./components/Onboarding";
import { SettingsPanel } from "./components/Settings";
import { useOnboardingStore } from "./stores/onboardingStore";
import { useSettingsStore } from "./stores/settingsStore";
/**
* Root application component.
*
* Shows the OnboardingWizard on first run, then the main app layout.
*/
export function App(): React.JSX.Element {
const toggleSettings = useSettingsStore((s) => s.togglePanel);
const isSettingsOpen = useSettingsStore((s) => s.isOpen);
const startConfigWatch = useSettingsStore((s) => s.startConfigWatch);
const stopConfigWatch = useSettingsStore((s) => s.stopConfigWatch);
const configReloadNotification = useSettingsStore(
(s) => s.configReloadNotification,
);
const clearConfigReloadNotification = useSettingsStore(
(s) => s.clearConfigReloadNotification,
);
import { useEffect, useState } from "react";
import { ChatPanel } from "./components/Chat";
import { FileBrowser } from "./components/FileBrowser";
import { OnboardingWizard } from "./components/Onboarding";
import { SettingsPanel } from "./components/Settings";
import { useOnboardingStore } from "./stores/onboardingStore";
/**
* Root application component.
*
* Shows the OnboardingWizard on first run, then the main app layout.
*/
export function App(): React.JSX.Element {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [configReloadNotification, setConfigReloadNotification] = useState<
string | null
>(null);
const toggleSettings = () => {
setIsSettingsOpen((prev) => !prev);
};
const startConfigWatch = () => {
// TODO: implement configuration file watching if needed.
};
const stopConfigWatch = () => {
// TODO: stop configuration file watching if implemented in startConfigWatch.
};
const clearConfigReloadNotification = () => {
setConfigReloadNotification(null);
};

Copilot uses AI. Check for mistakes.
const isOnboardingComplete = useOnboardingStore((s) => s.isComplete);

// Start/stop config file watching based on settings panel state
useEffect(() => {
if (isSettingsOpen) {
startConfigWatch();
} else {
stopConfigWatch();
}
return () => stopConfigWatch();
}, [isSettingsOpen, startConfigWatch, stopConfigWatch]);

if (!isOnboardingComplete) {
return <OnboardingWizard />;
}

return (
<div className="app-container">
{/* Config reload toast notification */}
{configReloadNotification && (
<div
className="config-reload-toast"
onClick={clearConfigReloadNotification}
>
<span className="toast-icon">🔄</span>
<span className="toast-message">{configReloadNotification}</span>
<button className="toast-close" aria-label="Dismiss">
×
</button>
</div>
)}

<header className="app-header">
<div className="app-title-group">
<div className="app-title-row">
<h1>LocalCowork</h1>
<span className="app-badge">on-device</span>
</div>
<span className="app-subtitle">
powered by LFM2-24B-A2B from Liquid AI
</span>
</div>
<div className="app-header-spacer" />
<button
className="app-settings-btn"
onClick={toggleSettings}
type="button"
title="Settings"
aria-label="Open settings"
>
&#9881;
</button>
</header>

<main className="app-main">
<FileBrowser />
<ChatPanel />
</main>

<footer className="app-footer">
<span>v0.1.0 &mdash; Agent Core</span>
</footer>

<SettingsPanel />
</div>
);
}
118 changes: 118 additions & 0 deletions src/components/Chat/MessageInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* MessageInput — text input area for sending messages.
*
* Supports Enter to send (Shift+Enter for newline) and disables
* input while the assistant is generating. Includes an InputToolbar
* below the textarea for folder context (Cowork-style "Work in a folder").
* Implements debouncing to prevent duplicate sends.
*/

import { useCallback, useRef, useState } from "react";

import { InputToolbar } from "./InputToolbar";
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MessageInput imports ./InputToolbar, but there is no InputToolbar module under src/components/Chat/ in this PR. Add the missing component or update the import to the correct path to avoid a module resolution/build error.

Suggested change
import { InputToolbar } from "./InputToolbar";
function InputToolbar(): React.JSX.Element {
return <div className="input-toolbar" />;
}

Copilot uses AI. Check for mistakes.

interface MessageInputProps {
readonly onSend: (content: string) => void;
readonly disabled: boolean;
}

/** Minimum time between send requests to prevent duplicates (500ms) */
const SEND_DEBOUNCE_MS = 500;

export function MessageInput({
onSend,
disabled,
}: MessageInputProps): React.JSX.Element {
const [value, setValue] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const lastSendTimeRef = useRef<number>(0);
const [isDebouncing, setIsDebouncing] = useState(false);

const handleSend = useCallback(() => {
const trimmed = value.trim();
if (!trimmed || disabled) return;

// Debounce: ignore clicks within 500ms
const now = Date.now();
if (now - lastSendTimeRef.current < SEND_DEBOUNCE_MS) {
setIsDebouncing(true);
setTimeout(() => setIsDebouncing(false), SEND_DEBOUNCE_MS);
return;
}
lastSendTimeRef.current = now;

onSend(trimmed);
setValue("");

// Reset textarea height
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
}
}, [value, disabled, onSend]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};

const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
setValue(e.target.value);

// Auto-resize textarea
const textarea = e.target;
textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
};

const isLoading = disabled || isDebouncing;

return (
<div className="message-input-wrapper">
<div className="message-input-row">
<textarea
ref={textareaRef}
className="message-input"
value={value}
onChange={handleInput}
onKeyDown={handleKeyDown}
placeholder={
isDebouncing
? "Please wait..."
: disabled
? "Waiting for response..."
: "Type a message..."
}
disabled={isLoading}
rows={1}
/>
<button
className={`send-button ${isDebouncing ? "debouncing" : ""}`}
onClick={handleSend}
disabled={isLoading || !value.trim()}
aria-label={isDebouncing ? "Please wait..." : "Send message"}
>
{isDebouncing ? (
<span className="send-button-spinner">⏳</span>
) : (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="22" y1="2" x2="11" y2="13" />
<polygon points="22 2 15 22 11 13 2 9 22 2" />
</svg>
)}
</button>
</div>
<InputToolbar />
</div>
);
}
Loading