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
|
isProcessing: boolean
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
|
runId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatInputInner({
|
function ChatInputInner({
|
||||||
|
|
@ -289,6 +290,7 @@ function ChatInputInner({
|
||||||
isProcessing,
|
isProcessing,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
|
runId,
|
||||||
}: ChatInputInnerProps) {
|
}: ChatInputInnerProps) {
|
||||||
const controller = usePromptInputController()
|
const controller = usePromptInputController()
|
||||||
const message = controller.textInput.value
|
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">
|
<div className="flex items-center gap-2 bg-background border border-border rounded-3xl shadow-xl px-4 py-2.5">
|
||||||
<PromptInputTextarea
|
<PromptInputTextarea
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
disabled={isProcessing}
|
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
autoFocus
|
||||||
|
focusTrigger={runId}
|
||||||
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
|
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -350,6 +353,7 @@ interface ChatInputWithMentionsProps {
|
||||||
isProcessing: boolean
|
isProcessing: boolean
|
||||||
presetMessage?: string
|
presetMessage?: string
|
||||||
onPresetMessageConsumed?: () => void
|
onPresetMessageConsumed?: () => void
|
||||||
|
runId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatInputWithMentions({
|
function ChatInputWithMentions({
|
||||||
|
|
@ -360,6 +364,7 @@ function ChatInputWithMentions({
|
||||||
isProcessing,
|
isProcessing,
|
||||||
presetMessage,
|
presetMessage,
|
||||||
onPresetMessageConsumed,
|
onPresetMessageConsumed,
|
||||||
|
runId,
|
||||||
}: ChatInputWithMentionsProps) {
|
}: ChatInputWithMentionsProps) {
|
||||||
return (
|
return (
|
||||||
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
|
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
|
||||||
|
|
@ -368,6 +373,7 @@ function ChatInputWithMentions({
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
presetMessage={presetMessage}
|
presetMessage={presetMessage}
|
||||||
onPresetMessageConsumed={onPresetMessageConsumed}
|
onPresetMessageConsumed={onPresetMessageConsumed}
|
||||||
|
runId={runId}
|
||||||
/>
|
/>
|
||||||
</PromptInputProvider>
|
</PromptInputProvider>
|
||||||
)
|
)
|
||||||
|
|
@ -376,6 +382,8 @@ function ChatInputWithMentions({
|
||||||
function App() {
|
function App() {
|
||||||
// File browser state (for Knowledge section)
|
// File browser state (for Knowledge section)
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||||
|
const [fileHistoryBack, setFileHistoryBack] = useState<string[]>([])
|
||||||
|
const [fileHistoryForward, setFileHistoryForward] = useState<string[]>([])
|
||||||
const [fileContent, setFileContent] = useState<string>('')
|
const [fileContent, setFileContent] = useState<string>('')
|
||||||
const [editorContent, setEditorContent] = useState<string>('')
|
const [editorContent, setEditorContent] = useState<string>('')
|
||||||
const [tree, setTree] = useState<TreeNode[]>([])
|
const [tree, setTree] = useState<TreeNode[]>([])
|
||||||
|
|
@ -404,6 +412,7 @@ function App() {
|
||||||
const [currentReasoning, setCurrentReasoning] = useState<string>('')
|
const [currentReasoning, setCurrentReasoning] = useState<string>('')
|
||||||
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
|
||||||
const [runId, setRunId] = useState<string | null>(null)
|
const [runId, setRunId] = useState<string | null>(null)
|
||||||
|
const runIdRef = useRef<string | null>(null)
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
const [agentId] = useState<string>('copilot')
|
const [agentId] = useState<string>('copilot')
|
||||||
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
|
||||||
|
|
@ -426,6 +435,11 @@ function App() {
|
||||||
// Onboarding state
|
// Onboarding state
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
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
|
// Load directory tree
|
||||||
const loadDirectory = useCallback(async () => {
|
const loadDirectory = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -722,15 +736,17 @@ function App() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Listen to run events
|
// Listen to run events
|
||||||
|
// Listen to run events - use ref to avoid stale closure issues
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
|
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
|
||||||
handleRunEvent(event as RunEventType)
|
handleRunEvent(event as RunEventType)
|
||||||
}) as (event: null) => void)
|
}) as (event: null) => void)
|
||||||
return cleanup
|
return cleanup
|
||||||
}, [runId])
|
}, [])
|
||||||
|
|
||||||
const handleRunEvent = (event: RunEventType) => {
|
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)
|
console.log('Run event:', event.type, event)
|
||||||
|
|
||||||
|
|
@ -1043,6 +1059,7 @@ function App() {
|
||||||
setRunId(null)
|
setRunId(null)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
setModelUsage(null)
|
setModelUsage(null)
|
||||||
|
setIsProcessing(false)
|
||||||
setPendingPermissionRequests(new Map())
|
setPendingPermissionRequests(new Map())
|
||||||
setPendingAskHumanRequests(new Map())
|
setPendingAskHumanRequests(new Map())
|
||||||
setAllPermissionRequests(new Map())
|
setAllPermissionRequests(new Map())
|
||||||
|
|
@ -1060,6 +1077,52 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
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
|
// Handle image upload for the markdown editor
|
||||||
const handleImageUpload = useCallback(async (file: File): Promise<string | null> => {
|
const handleImageUpload = useCallback(async (file: File): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1113,7 +1176,7 @@ function App() {
|
||||||
|
|
||||||
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
||||||
if (kind === 'file') {
|
if (kind === 'file') {
|
||||||
setSelectedPath(path)
|
navigateToFile(path)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1297,9 +1360,9 @@ function App() {
|
||||||
const openWikiLink = useCallback(async (wikiPath: string) => {
|
const openWikiLink = useCallback(async (wikiPath: string) => {
|
||||||
const resolvedPath = await ensureWikiFile(wikiPath)
|
const resolvedPath = await ensureWikiFile(wikiPath)
|
||||||
if (resolvedPath) {
|
if (resolvedPath) {
|
||||||
setSelectedPath(resolvedPath)
|
navigateToFile(resolvedPath)
|
||||||
}
|
}
|
||||||
}, [ensureWikiFile, setSelectedPath])
|
}, [ensureWikiFile, navigateToFile])
|
||||||
|
|
||||||
const wikiLinkConfig = React.useMemo(() => ({
|
const wikiLinkConfig = React.useMemo(() => ({
|
||||||
files: knowledgeFiles,
|
files: knowledgeFiles,
|
||||||
|
|
@ -1601,7 +1664,7 @@ function App() {
|
||||||
error={graphStatus === 'error' ? (graphError ?? 'Failed to build graph') : null}
|
error={graphStatus === 'error' ? (graphError ?? 'Failed to build graph') : null}
|
||||||
onSelectNode={(path) => {
|
onSelectNode={(path) => {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setSelectedPath(path)
|
navigateToFile(path)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1614,6 +1677,10 @@ function App() {
|
||||||
placeholder="Start writing..."
|
placeholder="Start writing..."
|
||||||
wikiLinks={wikiLinkConfig}
|
wikiLinks={wikiLinkConfig}
|
||||||
onImageUpload={handleImageUpload}
|
onImageUpload={handleImageUpload}
|
||||||
|
onNavigateBack={navigateBack}
|
||||||
|
onNavigateForward={navigateForward}
|
||||||
|
canNavigateBack={canNavigateBack}
|
||||||
|
canNavigateForward={canNavigateForward}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -1715,6 +1782,7 @@ function App() {
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
presetMessage={presetMessage}
|
presetMessage={presetMessage}
|
||||||
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
onPresetMessageConsumed={() => setPresetMessage(undefined)}
|
||||||
|
runId={runId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -904,13 +904,18 @@ export const PromptInputBody = ({
|
||||||
|
|
||||||
export type PromptInputTextareaProps = ComponentProps<
|
export type PromptInputTextareaProps = ComponentProps<
|
||||||
typeof InputGroupTextarea
|
typeof InputGroupTextarea
|
||||||
>;
|
> & {
|
||||||
|
autoFocus?: boolean;
|
||||||
|
focusTrigger?: unknown; // When this value changes, focus the textarea
|
||||||
|
};
|
||||||
|
|
||||||
export const PromptInputTextarea = ({
|
export const PromptInputTextarea = ({
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
placeholder = "What would you like to know?",
|
placeholder = "What would you like to know?",
|
||||||
onKeyDown: externalOnKeyDown,
|
onKeyDown: externalOnKeyDown,
|
||||||
|
autoFocus = false,
|
||||||
|
focusTrigger,
|
||||||
...props
|
...props
|
||||||
}: PromptInputTextareaProps) => {
|
}: PromptInputTextareaProps) => {
|
||||||
const controller = useOptionalPromptInputController();
|
const controller = useOptionalPromptInputController();
|
||||||
|
|
@ -920,6 +925,17 @@ export const PromptInputTextarea = ({
|
||||||
const [isComposing, setIsComposing] = useState(false);
|
const [isComposing, setIsComposing] = useState(false);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const highlightRef = useRef<HTMLDivElement>(null);
|
const highlightRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function Suggestions({
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex gap-2',
|
'flex gap-2',
|
||||||
vertical ? 'flex-col items-start' : 'flex-wrap justify-center',
|
vertical ? 'flex-col items-end' : 'flex-wrap justify-center',
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
{suggestions.map((suggestion) => (
|
{suggestions.map((suggestion) => (
|
||||||
|
|
|
||||||
|
|
@ -260,10 +260,16 @@ export function ChatSidebar({
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}, [width])
|
}, [width])
|
||||||
|
|
||||||
// Auto-focus textarea when sidebar opens
|
// Auto-focus textarea when sidebar opens or when conversation is cleared (new chat)
|
||||||
useEffect(() => {
|
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
|
// Auto-populate with @currentfile when switching knowledge files
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -584,9 +590,8 @@ export function ChatSidebar({
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onScroll={syncHighlightScroll}
|
onScroll={syncHighlightScroll}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
disabled={isProcessing}
|
|
||||||
rows={1}
|
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}
|
style={{ fieldSizing: 'content' } as React.CSSProperties}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ import {
|
||||||
MinusIcon,
|
MinusIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
CodeSquareIcon,
|
CodeSquareIcon,
|
||||||
Undo2Icon,
|
ChevronLeftIcon,
|
||||||
Redo2Icon,
|
ChevronRightIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
|
@ -33,9 +33,21 @@ interface EditorToolbarProps {
|
||||||
editor: Editor | null
|
editor: Editor | null
|
||||||
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
|
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
|
||||||
onImageUpload?: (file: File) => Promise<void> | 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 [linkUrl, setLinkUrl] = useState('')
|
||||||
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
@ -105,24 +117,24 @@ export function EditorToolbar({ editor, onSelectionHighlight, onImageUpload }: E
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-toolbar">
|
<div className="editor-toolbar">
|
||||||
{/* Undo / Redo */}
|
{/* Back / Forward Navigation */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
onClick={onNavigateBack}
|
||||||
disabled={!editor.can().undo()}
|
disabled={!canNavigateBack}
|
||||||
title="Undo (Ctrl+Z)"
|
title="Go back"
|
||||||
>
|
>
|
||||||
<Undo2Icon className="size-4" />
|
<ChevronLeftIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
onClick={onNavigateForward}
|
||||||
disabled={!editor.can().redo()}
|
disabled={!canNavigateForward}
|
||||||
title="Redo (Ctrl+Shift+Z)"
|
title="Go forward"
|
||||||
>
|
>
|
||||||
<Redo2Icon className="size-4" />
|
<ChevronRightIcon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="separator" />
|
<div className="separator" />
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ interface MarkdownEditorProps {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
wikiLinks?: WikiLinkConfig
|
wikiLinks?: WikiLinkConfig
|
||||||
onImageUpload?: (file: File) => Promise<string | null>
|
onImageUpload?: (file: File) => Promise<string | null>
|
||||||
|
onNavigateBack?: () => void
|
||||||
|
onNavigateForward?: () => void
|
||||||
|
canNavigateBack?: boolean
|
||||||
|
canNavigateForward?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type WikiLinkMatch = {
|
type WikiLinkMatch = {
|
||||||
|
|
@ -78,6 +82,10 @@ export function MarkdownEditor({
|
||||||
placeholder = 'Start writing...',
|
placeholder = 'Start writing...',
|
||||||
wikiLinks,
|
wikiLinks,
|
||||||
onImageUpload,
|
onImageUpload,
|
||||||
|
onNavigateBack,
|
||||||
|
onNavigateForward,
|
||||||
|
canNavigateBack,
|
||||||
|
canNavigateForward,
|
||||||
}: MarkdownEditorProps) {
|
}: MarkdownEditorProps) {
|
||||||
const isInternalUpdate = useRef(false)
|
const isInternalUpdate = useRef(false)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -318,7 +326,15 @@ export function MarkdownEditor({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
|
<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}>
|
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
{wikiLinks ? (
|
{wikiLinks ? (
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,6 @@ export function SettingsDialog({ children }: SettingsDialogProps) {
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="!max-w-[900px] w-[900px] h-[600px] p-0 gap-0 overflow-hidden"
|
className="!max-w-[900px] w-[900px] h-[600px] p-0 gap-0 overflow-hidden"
|
||||||
showCloseButton={false}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ const providerConfigs: ProviderConfig = {
|
||||||
},
|
},
|
||||||
scopes: [
|
scopes: [
|
||||||
'https://www.googleapis.com/auth/gmail.readonly',
|
'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',
|
'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 = [
|
const DEFAULT_ALLOW_LIST = [
|
||||||
"cat",
|
"cat",
|
||||||
"curl",
|
|
||||||
"date",
|
"date",
|
||||||
"echo",
|
"echo",
|
||||||
"grep",
|
"grep",
|
||||||
|
|
|
||||||
|
|
@ -997,6 +997,12 @@ If new info contradicts existing:
|
||||||
|
|
||||||
## 9a: Meetings — Create and Update Notes
|
## 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):**
|
**For new entities (meetings only):**
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
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
|
- Be concise: one line per activity entry
|
||||||
- Note state changes with \`[Field → value]\` in activity
|
- Note state changes with \`[Field → value]\` in activity
|
||||||
- Escape quotes properly in shell commands
|
- 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
|
## 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:**
|
**For new entities:**
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
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
|
- Use YYYY-MM-DD format for dates
|
||||||
- Be concise: one line per activity entry
|
- Be concise: one line per activity entry
|
||||||
- Escape quotes properly in shell commands
|
- 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
|
## 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):**
|
**For new entities (meetings and qualifying emails):**
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
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
|
- Be concise: one line per activity entry
|
||||||
- Note state changes with \`[Field → value]\` in activity
|
- Note state changes with \`[Field → value]\` in activity
|
||||||
- Escape quotes properly in shell commands
|
- 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 SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes
|
||||||
const LOOKBACK_DAYS = 14;
|
const LOOKBACK_DAYS = 14;
|
||||||
const REQUIRED_SCOPES = [
|
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'
|
'https://www.googleapis.com/auth/drive.readonly'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue