mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
commit
f5c8267762
13 changed files with 167 additions and 31 deletions
|
|
@ -282,6 +282,7 @@ interface ChatInputInnerProps {
|
|||
isProcessing: boolean
|
||||
presetMessage?: string
|
||||
onPresetMessageConsumed?: () => void
|
||||
runId?: string | null
|
||||
}
|
||||
|
||||
function ChatInputInner({
|
||||
|
|
@ -289,6 +290,7 @@ function ChatInputInner({
|
|||
isProcessing,
|
||||
presetMessage,
|
||||
onPresetMessageConsumed,
|
||||
runId,
|
||||
}: ChatInputInnerProps) {
|
||||
const controller = usePromptInputController()
|
||||
const message = controller.textInput.value
|
||||
|
|
@ -320,8 +322,9 @@ function ChatInputInner({
|
|||
<div className="flex items-center gap-2 bg-background border border-border rounded-3xl shadow-xl px-4 py-2.5">
|
||||
<PromptInputTextarea
|
||||
placeholder="Type your message..."
|
||||
disabled={isProcessing}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
focusTrigger={runId}
|
||||
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
|
||||
/>
|
||||
<Button
|
||||
|
|
@ -350,6 +353,7 @@ interface ChatInputWithMentionsProps {
|
|||
isProcessing: boolean
|
||||
presetMessage?: string
|
||||
onPresetMessageConsumed?: () => void
|
||||
runId?: string | null
|
||||
}
|
||||
|
||||
function ChatInputWithMentions({
|
||||
|
|
@ -360,6 +364,7 @@ function ChatInputWithMentions({
|
|||
isProcessing,
|
||||
presetMessage,
|
||||
onPresetMessageConsumed,
|
||||
runId,
|
||||
}: ChatInputWithMentionsProps) {
|
||||
return (
|
||||
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
|
||||
|
|
@ -368,6 +373,7 @@ function ChatInputWithMentions({
|
|||
isProcessing={isProcessing}
|
||||
presetMessage={presetMessage}
|
||||
onPresetMessageConsumed={onPresetMessageConsumed}
|
||||
runId={runId}
|
||||
/>
|
||||
</PromptInputProvider>
|
||||
)
|
||||
|
|
@ -376,6 +382,8 @@ function ChatInputWithMentions({
|
|||
function App() {
|
||||
// File browser state (for Knowledge section)
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||
const [fileHistoryBack, setFileHistoryBack] = useState<string[]>([])
|
||||
const [fileHistoryForward, setFileHistoryForward] = useState<string[]>([])
|
||||
const [fileContent, setFileContent] = useState<string>('')
|
||||
const [editorContent, setEditorContent] = useState<string>('')
|
||||
const [tree, setTree] = useState<TreeNode[]>([])
|
||||
|
|
@ -404,6 +412,7 @@ function App() {
|
|||
const [currentReasoning, setCurrentReasoning] = useState<string>('')
|
||||
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
||||
const [runId, setRunId] = useState<string | null>(null)
|
||||
const runIdRef = useRef<string | null>(null)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [agentId] = useState<string>('copilot')
|
||||
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
||||
|
|
@ -426,6 +435,11 @@ function App() {
|
|||
// Onboarding state
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
|
||||
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
|
||||
useEffect(() => {
|
||||
runIdRef.current = runId
|
||||
}, [runId])
|
||||
|
||||
// Load directory tree
|
||||
const loadDirectory = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -722,15 +736,17 @@ function App() {
|
|||
}, [])
|
||||
|
||||
// Listen to run events
|
||||
// Listen to run events - use ref to avoid stale closure issues
|
||||
useEffect(() => {
|
||||
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
|
||||
handleRunEvent(event as RunEventType)
|
||||
}) as (event: null) => void)
|
||||
return cleanup
|
||||
}, [runId])
|
||||
}, [])
|
||||
|
||||
const handleRunEvent = (event: RunEventType) => {
|
||||
if (event.runId !== runId) return
|
||||
// Use ref to get current runId to avoid stale closure issues
|
||||
if (event.runId !== runIdRef.current) return
|
||||
|
||||
console.log('Run event:', event.type, event)
|
||||
|
||||
|
|
@ -1043,6 +1059,7 @@ function App() {
|
|||
setRunId(null)
|
||||
setMessage('')
|
||||
setModelUsage(null)
|
||||
setIsProcessing(false)
|
||||
setPendingPermissionRequests(new Map())
|
||||
setPendingAskHumanRequests(new Map())
|
||||
setAllPermissionRequests(new Map())
|
||||
|
|
@ -1060,6 +1077,52 @@ function App() {
|
|||
setIsGraphOpen(false)
|
||||
}, [])
|
||||
|
||||
// File navigation with history tracking
|
||||
const navigateToFile = useCallback((path: string | null) => {
|
||||
if (path === selectedPath) return
|
||||
|
||||
// Push current path to back history (if we have one)
|
||||
if (selectedPath) {
|
||||
setFileHistoryBack(prev => [...prev, selectedPath])
|
||||
}
|
||||
// Clear forward history when navigating to a new file
|
||||
setFileHistoryForward([])
|
||||
setSelectedPath(path)
|
||||
}, [selectedPath])
|
||||
|
||||
const navigateBack = useCallback(() => {
|
||||
if (fileHistoryBack.length === 0) return
|
||||
|
||||
const newBack = [...fileHistoryBack]
|
||||
const previousPath = newBack.pop()!
|
||||
|
||||
// Push current path to forward history
|
||||
if (selectedPath) {
|
||||
setFileHistoryForward(prev => [...prev, selectedPath])
|
||||
}
|
||||
|
||||
setFileHistoryBack(newBack)
|
||||
setSelectedPath(previousPath)
|
||||
}, [fileHistoryBack, selectedPath])
|
||||
|
||||
const navigateForward = useCallback(() => {
|
||||
if (fileHistoryForward.length === 0) return
|
||||
|
||||
const newForward = [...fileHistoryForward]
|
||||
const nextPath = newForward.pop()!
|
||||
|
||||
// Push current path to back history
|
||||
if (selectedPath) {
|
||||
setFileHistoryBack(prev => [...prev, selectedPath])
|
||||
}
|
||||
|
||||
setFileHistoryForward(newForward)
|
||||
setSelectedPath(nextPath)
|
||||
}, [fileHistoryForward, selectedPath])
|
||||
|
||||
const canNavigateBack = fileHistoryBack.length > 0
|
||||
const canNavigateForward = fileHistoryForward.length > 0
|
||||
|
||||
// Handle image upload for the markdown editor
|
||||
const handleImageUpload = useCallback(async (file: File): Promise<string | null> => {
|
||||
try {
|
||||
|
|
@ -1113,7 +1176,7 @@ function App() {
|
|||
|
||||
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
||||
if (kind === 'file') {
|
||||
setSelectedPath(path)
|
||||
navigateToFile(path)
|
||||
setIsGraphOpen(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -1297,9 +1360,9 @@ function App() {
|
|||
const openWikiLink = useCallback(async (wikiPath: string) => {
|
||||
const resolvedPath = await ensureWikiFile(wikiPath)
|
||||
if (resolvedPath) {
|
||||
setSelectedPath(resolvedPath)
|
||||
navigateToFile(resolvedPath)
|
||||
}
|
||||
}, [ensureWikiFile, setSelectedPath])
|
||||
}, [ensureWikiFile, navigateToFile])
|
||||
|
||||
const wikiLinkConfig = React.useMemo(() => ({
|
||||
files: knowledgeFiles,
|
||||
|
|
@ -1601,7 +1664,7 @@ function App() {
|
|||
error={graphStatus === 'error' ? (graphError ?? 'Failed to build graph') : null}
|
||||
onSelectNode={(path) => {
|
||||
setIsGraphOpen(false)
|
||||
setSelectedPath(path)
|
||||
navigateToFile(path)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1614,6 +1677,10 @@ function App() {
|
|||
placeholder="Start writing..."
|
||||
wikiLinks={wikiLinkConfig}
|
||||
onImageUpload={handleImageUpload}
|
||||
onNavigateBack={navigateBack}
|
||||
onNavigateForward={navigateForward}
|
||||
canNavigateBack={canNavigateBack}
|
||||
canNavigateForward={canNavigateForward}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -1715,6 +1782,7 @@ function App() {
|
|||
isProcessing={isProcessing}
|
||||
presetMessage={presetMessage}
|
||||
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
||||
runId={runId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -904,13 +904,18 @@ export const PromptInputBody = ({
|
|||
|
||||
export type PromptInputTextareaProps = ComponentProps<
|
||||
typeof InputGroupTextarea
|
||||
>;
|
||||
> & {
|
||||
autoFocus?: boolean;
|
||||
focusTrigger?: unknown; // When this value changes, focus the textarea
|
||||
};
|
||||
|
||||
export const PromptInputTextarea = ({
|
||||
onChange,
|
||||
className,
|
||||
placeholder = "What would you like to know?",
|
||||
onKeyDown: externalOnKeyDown,
|
||||
autoFocus = false,
|
||||
focusTrigger,
|
||||
...props
|
||||
}: PromptInputTextareaProps) => {
|
||||
const controller = useOptionalPromptInputController();
|
||||
|
|
@ -920,6 +925,17 @@ export const PromptInputTextarea = ({
|
|||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Auto-focus the textarea when requested or when focusTrigger changes
|
||||
useEffect(() => {
|
||||
if (autoFocus || focusTrigger !== undefined) {
|
||||
// Small delay to ensure the element is fully mounted and visible
|
||||
const timer = setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [autoFocus, focusTrigger]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const highlightRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function Suggestions({
|
|||
return (
|
||||
<div className={cn(
|
||||
'flex gap-2',
|
||||
vertical ? 'flex-col items-start' : 'flex-wrap justify-center',
|
||||
vertical ? 'flex-col items-end' : 'flex-wrap justify-center',
|
||||
className
|
||||
)}>
|
||||
{suggestions.map((suggestion) => (
|
||||
|
|
|
|||
|
|
@ -260,10 +260,16 @@ export function ChatSidebar({
|
|||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}, [width])
|
||||
|
||||
// Auto-focus textarea when sidebar opens
|
||||
// Auto-focus textarea when sidebar opens or when conversation is cleared (new chat)
|
||||
useEffect(() => {
|
||||
textareaRef.current?.focus()
|
||||
}, [])
|
||||
// Focus when conversation is empty (new chat started)
|
||||
if (conversation.length === 0) {
|
||||
const timer = setTimeout(() => {
|
||||
textareaRef.current?.focus()
|
||||
}, 50)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [conversation.length])
|
||||
|
||||
// Auto-populate with @currentfile when switching knowledge files
|
||||
useEffect(() => {
|
||||
|
|
@ -584,9 +590,8 @@ export function ChatSidebar({
|
|||
onKeyDown={handleKeyDown}
|
||||
onScroll={syncHighlightScroll}
|
||||
placeholder="Ask anything..."
|
||||
disabled={isProcessing}
|
||||
rows={1}
|
||||
className="relative z-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:opacity-50 resize-none max-h-32 min-h-6"
|
||||
className="relative z-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground resize-none max-h-32 min-h-6"
|
||||
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import {
|
|||
MinusIcon,
|
||||
LinkIcon,
|
||||
CodeSquareIcon,
|
||||
Undo2Icon,
|
||||
Redo2Icon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ExternalLinkIcon,
|
||||
Trash2Icon,
|
||||
ImageIcon,
|
||||
|
|
@ -33,9 +33,21 @@ interface EditorToolbarProps {
|
|||
editor: Editor | null
|
||||
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
|
||||
onImageUpload?: (file: File) => Promise<void> | void
|
||||
onNavigateBack?: () => void
|
||||
onNavigateForward?: () => void
|
||||
canNavigateBack?: boolean
|
||||
canNavigateForward?: boolean
|
||||
}
|
||||
|
||||
export function EditorToolbar({ editor, onSelectionHighlight, onImageUpload }: EditorToolbarProps) {
|
||||
export function EditorToolbar({
|
||||
editor,
|
||||
onSelectionHighlight,
|
||||
onImageUpload,
|
||||
onNavigateBack,
|
||||
onNavigateForward,
|
||||
canNavigateBack,
|
||||
canNavigateForward,
|
||||
}: EditorToolbarProps) {
|
||||
const [linkUrl, setLinkUrl] = useState('')
|
||||
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
|
@ -105,24 +117,24 @@ export function EditorToolbar({ editor, onSelectionHighlight, onImageUpload }: E
|
|||
|
||||
return (
|
||||
<div className="editor-toolbar">
|
||||
{/* Undo / Redo */}
|
||||
{/* Back / Forward Navigation */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().undo()}
|
||||
title="Undo (Ctrl+Z)"
|
||||
onClick={onNavigateBack}
|
||||
disabled={!canNavigateBack}
|
||||
title="Go back"
|
||||
>
|
||||
<Undo2Icon className="size-4" />
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().redo()}
|
||||
title="Redo (Ctrl+Shift+Z)"
|
||||
onClick={onNavigateForward}
|
||||
disabled={!canNavigateForward}
|
||||
title="Go forward"
|
||||
>
|
||||
<Redo2Icon className="size-4" />
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<div className="separator" />
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ interface MarkdownEditorProps {
|
|||
placeholder?: string
|
||||
wikiLinks?: WikiLinkConfig
|
||||
onImageUpload?: (file: File) => Promise<string | null>
|
||||
onNavigateBack?: () => void
|
||||
onNavigateForward?: () => void
|
||||
canNavigateBack?: boolean
|
||||
canNavigateForward?: boolean
|
||||
}
|
||||
|
||||
type WikiLinkMatch = {
|
||||
|
|
@ -78,6 +82,10 @@ export function MarkdownEditor({
|
|||
placeholder = 'Start writing...',
|
||||
wikiLinks,
|
||||
onImageUpload,
|
||||
onNavigateBack,
|
||||
onNavigateForward,
|
||||
canNavigateBack,
|
||||
canNavigateForward,
|
||||
}: MarkdownEditorProps) {
|
||||
const isInternalUpdate = useRef(false)
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
|
|
@ -318,7 +326,15 @@ export function MarkdownEditor({
|
|||
|
||||
return (
|
||||
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
|
||||
<EditorToolbar editor={editor} onSelectionHighlight={setSelectionHighlight} onImageUpload={handleImageUploadWithPlaceholder} />
|
||||
<EditorToolbar
|
||||
editor={editor}
|
||||
onSelectionHighlight={setSelectionHighlight}
|
||||
onImageUpload={handleImageUploadWithPlaceholder}
|
||||
onNavigateBack={onNavigateBack}
|
||||
onNavigateForward={onNavigateForward}
|
||||
canNavigateBack={canNavigateBack}
|
||||
canNavigateForward={canNavigateForward}
|
||||
/>
|
||||
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
|
||||
<EditorContent editor={editor} />
|
||||
{wikiLinks ? (
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ export function SettingsDialog({ children }: SettingsDialogProps) {
|
|||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="!max-w-[900px] w-[900px] h-[600px] p-0 gap-0 overflow-hidden"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
{/* Sidebar */}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const providerConfigs: ProviderConfig = {
|
|||
},
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
'https://www.googleapis.com/auth/calendar.events.readonly',
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json"
|
|||
|
||||
const DEFAULT_ALLOW_LIST = [
|
||||
"cat",
|
||||
"curl",
|
||||
"date",
|
||||
"echo",
|
||||
"grep",
|
||||
|
|
|
|||
|
|
@ -997,6 +997,12 @@ If new info contradicts existing:
|
|||
|
||||
## 9a: Meetings — Create and Update Notes
|
||||
|
||||
**IMPORTANT: Write sequentially, one file at a time.**
|
||||
- Generate content for exactly one note.
|
||||
- Issue exactly one \`write\` command.
|
||||
- Wait for the tool to return before generating the next note.
|
||||
- Do NOT batch multiple \`write\` commands in a single response.
|
||||
|
||||
**For new entities (meetings only):**
|
||||
\`\`\`bash
|
||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
||||
|
|
@ -1103,6 +1109,7 @@ If you discovered new name variants during resolution, add them to Aliases field
|
|||
- Be concise: one line per activity entry
|
||||
- Note state changes with \`[Field → value]\` in activity
|
||||
- Escape quotes properly in shell commands
|
||||
- Write only one file per response (no multi-file write batches)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -553,6 +553,12 @@ Before writing:
|
|||
|
||||
## 9a: Create and Update Notes
|
||||
|
||||
**IMPORTANT: Write sequentially, one file at a time.**
|
||||
- Generate content for exactly one note.
|
||||
- Issue exactly one \`write\` command.
|
||||
- Wait for the tool to return before generating the next note.
|
||||
- Do NOT batch multiple \`write\` commands in a single response.
|
||||
|
||||
**For new entities:**
|
||||
\`\`\`bash
|
||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
||||
|
|
@ -579,6 +585,7 @@ Add newly discovered name variants to Aliases field.
|
|||
- Use YYYY-MM-DD format for dates
|
||||
- Be concise: one line per activity entry
|
||||
- Escape quotes properly in shell commands
|
||||
- Write only one file per response (no multi-file write batches)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -906,6 +906,12 @@ If new info contradicts existing:
|
|||
|
||||
## 9a: Create and Update Notes
|
||||
|
||||
**IMPORTANT: Write sequentially, one file at a time.**
|
||||
- Generate content for exactly one note.
|
||||
- Issue exactly one \`write\` command.
|
||||
- Wait for the tool to return before generating the next note.
|
||||
- Do NOT batch multiple \`write\` commands in a single response.
|
||||
|
||||
**For new entities (meetings and qualifying emails):**
|
||||
\`\`\`bash
|
||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
||||
|
|
@ -941,6 +947,7 @@ If you discovered new name variants during resolution, add them to Aliases field
|
|||
- Be concise: one line per activity entry
|
||||
- Note state changes with \`[Field → value]\` in activity
|
||||
- Escape quotes properly in shell commands
|
||||
- Write only one file per response (no multi-file write batches)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const SYNC_DIR = path.join(WorkDir, 'calendar_sync');
|
|||
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
|
||||
const LOOKBACK_DAYS = 14;
|
||||
const REQUIRED_SCOPES = [
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
'https://www.googleapis.com/auth/calendar.events.readonly',
|
||||
'https://www.googleapis.com/auth/drive.readonly'
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue