Skip to content

Commit f5c8267

Browse files
authored
Merge pull request #331 from rowboatlabs/dev
Dev
2 parents 9084c73 + 7f91e88 commit f5c8267

File tree

13 files changed

+167
-31
lines changed

13 files changed

+167
-31
lines changed

apps/x/apps/renderer/src/App.tsx

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,13 +282,15 @@ interface ChatInputInnerProps {
282282
isProcessing: boolean
283283
presetMessage?: string
284284
onPresetMessageConsumed?: () => void
285+
runId?: string | null
285286
}
286287

287288
function ChatInputInner({
288289
onSubmit,
289290
isProcessing,
290291
presetMessage,
291292
onPresetMessageConsumed,
293+
runId,
292294
}: ChatInputInnerProps) {
293295
const controller = usePromptInputController()
294296
const message = controller.textInput.value
@@ -320,8 +322,9 @@ function ChatInputInner({
320322
<div className="flex items-center gap-2 bg-background border border-border rounded-3xl shadow-xl px-4 py-2.5">
321323
<PromptInputTextarea
322324
placeholder="Type your message..."
323-
disabled={isProcessing}
324325
onKeyDown={handleKeyDown}
326+
autoFocus
327+
focusTrigger={runId}
325328
className="min-h-6 py-0 border-0 shadow-none focus-visible:ring-0 rounded-none"
326329
/>
327330
<Button
@@ -350,6 +353,7 @@ interface ChatInputWithMentionsProps {
350353
isProcessing: boolean
351354
presetMessage?: string
352355
onPresetMessageConsumed?: () => void
356+
runId?: string | null
353357
}
354358

355359
function ChatInputWithMentions({
@@ -360,6 +364,7 @@ function ChatInputWithMentions({
360364
isProcessing,
361365
presetMessage,
362366
onPresetMessageConsumed,
367+
runId,
363368
}: ChatInputWithMentionsProps) {
364369
return (
365370
<PromptInputProvider knowledgeFiles={knowledgeFiles} recentFiles={recentFiles} visibleFiles={visibleFiles}>
@@ -368,6 +373,7 @@ function ChatInputWithMentions({
368373
isProcessing={isProcessing}
369374
presetMessage={presetMessage}
370375
onPresetMessageConsumed={onPresetMessageConsumed}
376+
runId={runId}
371377
/>
372378
</PromptInputProvider>
373379
)
@@ -376,6 +382,8 @@ function ChatInputWithMentions({
376382
function App() {
377383
// File browser state (for Knowledge section)
378384
const [selectedPath, setSelectedPath] = useState<string | null>(null)
385+
const [fileHistoryBack, setFileHistoryBack] = useState<string[]>([])
386+
const [fileHistoryForward, setFileHistoryForward] = useState<string[]>([])
379387
const [fileContent, setFileContent] = useState<string>('')
380388
const [editorContent, setEditorContent] = useState<string>('')
381389
const [tree, setTree] = useState<TreeNode[]>([])
@@ -404,6 +412,7 @@ function App() {
404412
const [currentReasoning, setCurrentReasoning] = useState<string>('')
405413
const [, setModelUsage] = useState<LanguageModelUsage | null>(null)
406414
const [runId, setRunId] = useState<string | null>(null)
415+
const runIdRef = useRef<string | null>(null)
407416
const [isProcessing, setIsProcessing] = useState(false)
408417
const [agentId] = useState<string>('copilot')
409418
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
@@ -426,6 +435,11 @@ function App() {
426435
// Onboarding state
427436
const [showOnboarding, setShowOnboarding] = useState(false)
428437

438+
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
439+
useEffect(() => {
440+
runIdRef.current = runId
441+
}, [runId])
442+
429443
// Load directory tree
430444
const loadDirectory = useCallback(async () => {
431445
try {
@@ -722,15 +736,17 @@ function App() {
722736
}, [])
723737

724738
// Listen to run events
739+
// Listen to run events - use ref to avoid stale closure issues
725740
useEffect(() => {
726741
const cleanup = window.ipc.on('runs:events', ((event: unknown) => {
727742
handleRunEvent(event as RunEventType)
728743
}) as (event: null) => void)
729744
return cleanup
730-
}, [runId])
745+
}, [])
731746

732747
const handleRunEvent = (event: RunEventType) => {
733-
if (event.runId !== runId) return
748+
// Use ref to get current runId to avoid stale closure issues
749+
if (event.runId !== runIdRef.current) return
734750

735751
console.log('Run event:', event.type, event)
736752

@@ -1043,6 +1059,7 @@ function App() {
10431059
setRunId(null)
10441060
setMessage('')
10451061
setModelUsage(null)
1062+
setIsProcessing(false)
10461063
setPendingPermissionRequests(new Map())
10471064
setPendingAskHumanRequests(new Map())
10481065
setAllPermissionRequests(new Map())
@@ -1060,6 +1077,52 @@ function App() {
10601077
setIsGraphOpen(false)
10611078
}, [])
10621079

1080+
// File navigation with history tracking
1081+
const navigateToFile = useCallback((path: string | null) => {
1082+
if (path === selectedPath) return
1083+
1084+
// Push current path to back history (if we have one)
1085+
if (selectedPath) {
1086+
setFileHistoryBack(prev => [...prev, selectedPath])
1087+
}
1088+
// Clear forward history when navigating to a new file
1089+
setFileHistoryForward([])
1090+
setSelectedPath(path)
1091+
}, [selectedPath])
1092+
1093+
const navigateBack = useCallback(() => {
1094+
if (fileHistoryBack.length === 0) return
1095+
1096+
const newBack = [...fileHistoryBack]
1097+
const previousPath = newBack.pop()!
1098+
1099+
// Push current path to forward history
1100+
if (selectedPath) {
1101+
setFileHistoryForward(prev => [...prev, selectedPath])
1102+
}
1103+
1104+
setFileHistoryBack(newBack)
1105+
setSelectedPath(previousPath)
1106+
}, [fileHistoryBack, selectedPath])
1107+
1108+
const navigateForward = useCallback(() => {
1109+
if (fileHistoryForward.length === 0) return
1110+
1111+
const newForward = [...fileHistoryForward]
1112+
const nextPath = newForward.pop()!
1113+
1114+
// Push current path to back history
1115+
if (selectedPath) {
1116+
setFileHistoryBack(prev => [...prev, selectedPath])
1117+
}
1118+
1119+
setFileHistoryForward(newForward)
1120+
setSelectedPath(nextPath)
1121+
}, [fileHistoryForward, selectedPath])
1122+
1123+
const canNavigateBack = fileHistoryBack.length > 0
1124+
const canNavigateForward = fileHistoryForward.length > 0
1125+
10631126
// Handle image upload for the markdown editor
10641127
const handleImageUpload = useCallback(async (file: File): Promise<string | null> => {
10651128
try {
@@ -1113,7 +1176,7 @@ function App() {
11131176

11141177
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
11151178
if (kind === 'file') {
1116-
setSelectedPath(path)
1179+
navigateToFile(path)
11171180
setIsGraphOpen(false)
11181181
return
11191182
}
@@ -1297,9 +1360,9 @@ function App() {
12971360
const openWikiLink = useCallback(async (wikiPath: string) => {
12981361
const resolvedPath = await ensureWikiFile(wikiPath)
12991362
if (resolvedPath) {
1300-
setSelectedPath(resolvedPath)
1363+
navigateToFile(resolvedPath)
13011364
}
1302-
}, [ensureWikiFile, setSelectedPath])
1365+
}, [ensureWikiFile, navigateToFile])
13031366

13041367
const wikiLinkConfig = React.useMemo(() => ({
13051368
files: knowledgeFiles,
@@ -1601,7 +1664,7 @@ function App() {
16011664
error={graphStatus === 'error' ? (graphError ?? 'Failed to build graph') : null}
16021665
onSelectNode={(path) => {
16031666
setIsGraphOpen(false)
1604-
setSelectedPath(path)
1667+
navigateToFile(path)
16051668
}}
16061669
/>
16071670
</div>
@@ -1614,6 +1677,10 @@ function App() {
16141677
placeholder="Start writing..."
16151678
wikiLinks={wikiLinkConfig}
16161679
onImageUpload={handleImageUpload}
1680+
onNavigateBack={navigateBack}
1681+
onNavigateForward={navigateForward}
1682+
canNavigateBack={canNavigateBack}
1683+
canNavigateForward={canNavigateForward}
16171684
/>
16181685
</div>
16191686
) : (
@@ -1715,6 +1782,7 @@ function App() {
17151782
isProcessing={isProcessing}
17161783
presetMessage={presetMessage}
17171784
onPresetMessageConsumed={() => setPresetMessage(undefined)}
1785+
runId={runId}
17181786
/>
17191787
</div>
17201788
</div>

apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,13 +904,18 @@ export const PromptInputBody = ({
904904

905905
export type PromptInputTextareaProps = ComponentProps<
906906
typeof InputGroupTextarea
907-
>;
907+
> & {
908+
autoFocus?: boolean;
909+
focusTrigger?: unknown; // When this value changes, focus the textarea
910+
};
908911

909912
export const PromptInputTextarea = ({
910913
onChange,
911914
className,
912915
placeholder = "What would you like to know?",
913916
onKeyDown: externalOnKeyDown,
917+
autoFocus = false,
918+
focusTrigger,
914919
...props
915920
}: PromptInputTextareaProps) => {
916921
const controller = useOptionalPromptInputController();
@@ -920,6 +925,17 @@ export const PromptInputTextarea = ({
920925
const [isComposing, setIsComposing] = useState(false);
921926

922927
const textareaRef = useRef<HTMLTextAreaElement>(null);
928+
929+
// Auto-focus the textarea when requested or when focusTrigger changes
930+
useEffect(() => {
931+
if (autoFocus || focusTrigger !== undefined) {
932+
// Small delay to ensure the element is fully mounted and visible
933+
const timer = setTimeout(() => {
934+
textareaRef.current?.focus();
935+
}, 50);
936+
return () => clearTimeout(timer);
937+
}
938+
}, [autoFocus, focusTrigger]);
923939
const containerRef = useRef<HTMLDivElement>(null);
924940
const highlightRef = useRef<HTMLDivElement>(null);
925941

apps/x/apps/renderer/src/components/ai-elements/suggestions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function Suggestions({
5151
return (
5252
<div className={cn(
5353
'flex gap-2',
54-
vertical ? 'flex-col items-start' : 'flex-wrap justify-center',
54+
vertical ? 'flex-col items-end' : 'flex-wrap justify-center',
5555
className
5656
)}>
5757
{suggestions.map((suggestion) => (

apps/x/apps/renderer/src/components/chat-sidebar.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,16 @@ export function ChatSidebar({
260260
document.addEventListener('mouseup', handleMouseUp)
261261
}, [width])
262262

263-
// Auto-focus textarea when sidebar opens
263+
// Auto-focus textarea when sidebar opens or when conversation is cleared (new chat)
264264
useEffect(() => {
265-
textareaRef.current?.focus()
266-
}, [])
265+
// Focus when conversation is empty (new chat started)
266+
if (conversation.length === 0) {
267+
const timer = setTimeout(() => {
268+
textareaRef.current?.focus()
269+
}, 50)
270+
return () => clearTimeout(timer)
271+
}
272+
}, [conversation.length])
267273

268274
// Auto-populate with @currentfile when switching knowledge files
269275
useEffect(() => {
@@ -584,9 +590,8 @@ export function ChatSidebar({
584590
onKeyDown={handleKeyDown}
585591
onScroll={syncHighlightScroll}
586592
placeholder="Ask anything..."
587-
disabled={isProcessing}
588593
rows={1}
589-
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"
594+
className="relative z-10 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground resize-none max-h-32 min-h-6"
590595
style={{ fieldSizing: 'content' } as React.CSSProperties}
591596
/>
592597
</div>

apps/x/apps/renderer/src/components/editor-toolbar.tsx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
MinusIcon,
2323
LinkIcon,
2424
CodeSquareIcon,
25-
Undo2Icon,
26-
Redo2Icon,
25+
ChevronLeftIcon,
26+
ChevronRightIcon,
2727
ExternalLinkIcon,
2828
Trash2Icon,
2929
ImageIcon,
@@ -33,9 +33,21 @@ interface EditorToolbarProps {
3333
editor: Editor | null
3434
onSelectionHighlight?: (range: { from: number; to: number } | null) => void
3535
onImageUpload?: (file: File) => Promise<void> | void
36+
onNavigateBack?: () => void
37+
onNavigateForward?: () => void
38+
canNavigateBack?: boolean
39+
canNavigateForward?: boolean
3640
}
3741

38-
export function EditorToolbar({ editor, onSelectionHighlight, onImageUpload }: EditorToolbarProps) {
42+
export function EditorToolbar({
43+
editor,
44+
onSelectionHighlight,
45+
onImageUpload,
46+
onNavigateBack,
47+
onNavigateForward,
48+
canNavigateBack,
49+
canNavigateForward,
50+
}: EditorToolbarProps) {
3951
const [linkUrl, setLinkUrl] = useState('')
4052
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
4153
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -105,24 +117,24 @@ export function EditorToolbar({ editor, onSelectionHighlight, onImageUpload }: E
105117

106118
return (
107119
<div className="editor-toolbar">
108-
{/* Undo / Redo */}
120+
{/* Back / Forward Navigation */}
109121
<Button
110122
variant="ghost"
111123
size="icon-sm"
112-
onClick={() => editor.chain().focus().undo().run()}
113-
disabled={!editor.can().undo()}
114-
title="Undo (Ctrl+Z)"
124+
onClick={onNavigateBack}
125+
disabled={!canNavigateBack}
126+
title="Go back"
115127
>
116-
<Undo2Icon className="size-4" />
128+
<ChevronLeftIcon className="size-4" />
117129
</Button>
118130
<Button
119131
variant="ghost"
120132
size="icon-sm"
121-
onClick={() => editor.chain().focus().redo().run()}
122-
disabled={!editor.can().redo()}
123-
title="Redo (Ctrl+Shift+Z)"
133+
onClick={onNavigateForward}
134+
disabled={!canNavigateForward}
135+
title="Go forward"
124136
>
125-
<Redo2Icon className="size-4" />
137+
<ChevronRightIcon className="size-4" />
126138
</Button>
127139

128140
<div className="separator" />

apps/x/apps/renderer/src/components/markdown-editor.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ interface MarkdownEditorProps {
3030
placeholder?: string
3131
wikiLinks?: WikiLinkConfig
3232
onImageUpload?: (file: File) => Promise<string | null>
33+
onNavigateBack?: () => void
34+
onNavigateForward?: () => void
35+
canNavigateBack?: boolean
36+
canNavigateForward?: boolean
3337
}
3438

3539
type WikiLinkMatch = {
@@ -78,6 +82,10 @@ export function MarkdownEditor({
7882
placeholder = 'Start writing...',
7983
wikiLinks,
8084
onImageUpload,
85+
onNavigateBack,
86+
onNavigateForward,
87+
canNavigateBack,
88+
canNavigateForward,
8189
}: MarkdownEditorProps) {
8290
const isInternalUpdate = useRef(false)
8391
const wrapperRef = useRef<HTMLDivElement>(null)
@@ -318,7 +326,15 @@ export function MarkdownEditor({
318326

319327
return (
320328
<div className="tiptap-editor" onKeyDown={handleKeyDown}>
321-
<EditorToolbar editor={editor} onSelectionHighlight={setSelectionHighlight} onImageUpload={handleImageUploadWithPlaceholder} />
329+
<EditorToolbar
330+
editor={editor}
331+
onSelectionHighlight={setSelectionHighlight}
332+
onImageUpload={handleImageUploadWithPlaceholder}
333+
onNavigateBack={onNavigateBack}
334+
onNavigateForward={onNavigateForward}
335+
canNavigateBack={canNavigateBack}
336+
canNavigateForward={canNavigateForward}
337+
/>
322338
<div className="editor-content-wrapper" ref={wrapperRef} onScroll={handleScroll}>
323339
<EditorContent editor={editor} />
324340
{wikiLinks ? (

0 commit comments

Comments
 (0)