diff --git a/apps/code/src/main/services/fs/schemas.ts b/apps/code/src/main/services/fs/schemas.ts index fec9d26df..2981517b4 100644 --- a/apps/code/src/main/services/fs/schemas.ts +++ b/apps/code/src/main/services/fs/schemas.ts @@ -29,6 +29,7 @@ const fileEntry = z.object({ export const listRepoFilesOutput = z.array(fileEntry); export const readRepoFileOutput = z.string().nullable(); +export const fileExistsOutput = z.boolean(); export type ListRepoFilesInput = z.infer; export type ReadRepoFileInput = z.infer; diff --git a/apps/code/src/main/services/fs/service.ts b/apps/code/src/main/services/fs/service.ts index 2e00f0d91..52e0844b8 100644 --- a/apps/code/src/main/services/fs/service.ts +++ b/apps/code/src/main/services/fs/service.ts @@ -97,6 +97,15 @@ export class FsService { } } + async fileExists(filePath: string): Promise { + try { + const stat = await fs.promises.stat(path.resolve(filePath)); + return stat.isFile(); + } catch { + return false; + } + } + async readAbsoluteFile(filePath: string): Promise { try { return await fs.promises.readFile(path.resolve(filePath), "utf-8"); diff --git a/apps/code/src/main/trpc/routers/fs.ts b/apps/code/src/main/trpc/routers/fs.ts index 1aede0b35..81379c81b 100644 --- a/apps/code/src/main/trpc/routers/fs.ts +++ b/apps/code/src/main/trpc/routers/fs.ts @@ -1,6 +1,7 @@ import { container } from "../../di/container"; import { MAIN_TOKENS } from "../../di/tokens"; import { + fileExistsOutput, listRepoFilesInput, listRepoFilesOutput, readAbsoluteFileInput, @@ -33,6 +34,11 @@ export const fsRouter = router({ .output(readRepoFileOutput) .query(({ input }) => getService().readAbsoluteFile(input.filePath)), + fileExists: publicProcedure + .input(readAbsoluteFileInput) + .output(fileExistsOutput) + .query(({ input }) => getService().fileExists(input.filePath)), + readFileAsBase64: publicProcedure .input(readAbsoluteFileInput) .output(readRepoFileOutput) diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 6bdd895bc..30b8903f4 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -123,7 +123,7 @@ export function createWindow(): void { ...(savedState.y !== undefined && { y: savedState.y }), width: savedState.width, height: savedState.height, - minWidth: 1200, + minWidth: 480, minHeight: 600, backgroundColor: "#0a0a0a", ...platformWindowConfig, diff --git a/apps/code/src/renderer/components/action-selector/InlineEditableText.tsx b/apps/code/src/renderer/components/action-selector/InlineEditableText.tsx index e8e66cf6b..c33e9e05d 100644 --- a/apps/code/src/renderer/components/action-selector/InlineEditableText.tsx +++ b/apps/code/src/renderer/components/action-selector/InlineEditableText.tsx @@ -87,6 +87,7 @@ export function InlineEditableText({ overflow: "auto", maxHeight: "120px", wordBreak: "break-word", + cursor: "text", userSelect: active ? "auto" : "none", pointerEvents: active ? "auto" : "none", }} diff --git a/apps/code/src/renderer/features/message-editor/components/AttachmentsBar.tsx b/apps/code/src/renderer/features/message-editor/components/AttachmentsBar.tsx index 15423d001..f40b64abf 100644 --- a/apps/code/src/renderer/features/message-editor/components/AttachmentsBar.tsx +++ b/apps/code/src/renderer/features/message-editor/components/AttachmentsBar.tsx @@ -153,7 +153,11 @@ export function AttachmentsBar({ attachments, onRemove }: AttachmentsBarProps) { if (attachments.length === 0) return null; return ( - + {attachments.map((att) => isImageFile(att.label) ? ( @@ -270,19 +270,23 @@ export const MessageEditor = forwardRef( direction="column" gap="2" onClick={handleContainerClick} - className={`rounded-md p-2 ${isBashMode ? "ring-1 ring-blue-9" : ""}`} + className={`rounded-md py-2 pr-0 pl-1.5 ${isBashMode ? "ring-1 ring-blue-9" : ""}`} style={{ cursor: "text" }} >
- + result.item); } +async function getAbsolutePathSuggestion( + query: string, +): Promise { + if (!isAbsolutePath(query)) return null; + + try { + const exists = await trpcClient.fs.fileExists.query({ filePath: query }); + if (!exists) return null; + } catch { + return null; + } + + const fileItem = pathToFileItem(query); + return { + id: query, + label: fileItem.name, + description: fileItem.dir || undefined, + filename: fileItem.name, + path: query, + }; +} + export async function getFileSuggestions( sessionId: string, query: string, ): Promise { const repoPath = useDraftStore.getState().contexts[sessionId]?.repoPath; + const absolutePathSuggestion = getAbsolutePathSuggestion(query); if (!repoPath) { - return []; + const resolved = await absolutePathSuggestion; + return resolved ? [resolved] : []; } const { files, fzf } = await fetchRepoFiles(repoPath); const matched = searchFiles(fzf, files, query); - return matched.map((file) => ({ - id: file.path, - label: file.name, - description: file.dir || undefined, - filename: file.name, - path: file.path, - })); + const results: FileSuggestionItem[] = matched.map((file) => { + const parentDir = file.dir ? file.dir.split("/").pop() : undefined; + const label = parentDir ? `${parentDir}/${file.name}` : file.name; + + return { + id: file.path, + label, + description: file.dir || undefined, + filename: file.name, + path: file.path, + }; + }); + + const resolved = await absolutePathSuggestion; + if (resolved && !results.some((r) => r.id === resolved.id)) { + results.unshift(resolved); + } + + return results; } export function getCommandSuggestions( diff --git a/apps/code/src/renderer/features/message-editor/tiptap/CommandMention.ts b/apps/code/src/renderer/features/message-editor/tiptap/CommandMention.ts index 1c68e308c..dccdbc3d6 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/CommandMention.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/CommandMention.ts @@ -23,9 +23,11 @@ function createSuggestion( render: () => { let component: ReactRenderer | null = null; let popup: TippyInstance | null = null; + let dismissed = false; return { onStart: (props) => { + dismissed = false; component = new ReactRenderer(SuggestionList, { props: { items: props.items, @@ -67,9 +69,12 @@ function createSuggestion( if (props.event.key === "Escape") { props.event.stopPropagation(); popup?.hide(); + dismissed = true; return true; } + if (dismissed) return false; + return component?.ref?.onKeyDown(props) ?? false; }, diff --git a/apps/code/src/renderer/features/message-editor/tiptap/FileMention.ts b/apps/code/src/renderer/features/message-editor/tiptap/FileMention.ts index 3ba26bed4..2eaece93d 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/FileMention.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/FileMention.ts @@ -27,9 +27,11 @@ function createSuggestion( render: () => { let component: ReactRenderer | null = null; let popup: TippyInstance | null = null; + let dismissed = false; return { onStart: (props) => { + dismissed = false; const items = props.items.length > 0 ? props.items : lastItems; component = new ReactRenderer(SuggestionList, { props: { @@ -73,9 +75,12 @@ function createSuggestion( if (props.event.key === "Escape") { props.event.stopPropagation(); popup?.hide(); + dismissed = true; return true; } + if (dismissed) return false; + return component?.ref?.onKeyDown(props) ?? false; }, diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx index 0a459455d..ae1733913 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx +++ b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx @@ -35,8 +35,9 @@ function DefaultChip({ const isCommand = type === "command"; const prefix = isCommand ? "/" : "@"; + const isFile = type === "file"; - return ( + const chip = ( ); + + if (isFile) { + return {chip}; + } + + return chip; } function PastedTextChip({ diff --git a/apps/code/src/renderer/features/message-editor/tiptap/SuggestionList.tsx b/apps/code/src/renderer/features/message-editor/tiptap/SuggestionList.tsx index aed1a863e..3d4622f84 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/SuggestionList.tsx +++ b/apps/code/src/renderer/features/message-editor/tiptap/SuggestionList.tsx @@ -57,8 +57,11 @@ export const SuggestionList = forwardRef< return true; } if (event.key === "Enter" || event.key === "Tab") { - if (items[selectedIndex]) command(items[selectedIndex]); - return true; + if (items[selectedIndex]) { + command(items[selectedIndex]); + return true; + } + return false; } return false; }, diff --git a/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts b/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts index 7a8a7d47a..408c2738c 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts +++ b/apps/code/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts @@ -38,7 +38,7 @@ export interface UseTiptapEditorOptions { } const EDITOR_CLASS = - "cli-editor min-h-[1.5em] w-full break-words border-none bg-transparent text-[13px] text-[var(--gray-12)] outline-none [overflow-wrap:break-word] [white-space:pre-wrap] [word-break:break-word]"; + "cli-editor min-h-[1.5em] w-full break-words border-none bg-transparent pr-2 text-[13px] text-[var(--gray-12)] outline-none [overflow-wrap:break-word] [white-space:pre-wrap] [word-break:break-word]"; async function pasteTextAsFile( view: EditorView, @@ -187,9 +187,10 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) { if (isSubmitKey) { if (!view.editable || submitDisabledRef.current) return false; - const suggestionPopup = - document.querySelector("[data-tippy-root]"); - if (suggestionPopup) return false; + const visibleSuggestion = document.querySelector( + "[data-tippy-root] .tippy-box:not([data-state='hidden'])", + ); + if (visibleSuggestion) return false; event.preventDefault(); historyActions.reset(); submitRef.current(); diff --git a/apps/code/src/renderer/features/sessions/components/GeneratingIndicator.tsx b/apps/code/src/renderer/features/sessions/components/GeneratingIndicator.tsx index b24a86a7b..f69963549 100644 --- a/apps/code/src/renderer/features/sessions/components/GeneratingIndicator.tsx +++ b/apps/code/src/renderer/features/sessions/components/GeneratingIndicator.tsx @@ -151,7 +151,7 @@ export function GeneratingIndicator({ {activity}... - (Esc to interrupt + (Esc to stop - + {CATEGORY_TITLES[activeCategory]} diff --git a/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx index 11000245b..c4839a738 100644 --- a/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/ClaudeCodeSettings.tsx @@ -178,8 +178,8 @@ export function ClaudeCodeSettings() { - Bypass Permissions mode is enabled. All permission rules are - ignored. + Auto-accept is enabled. Use shift+tab to cycle to this mode in each + session. )} @@ -210,14 +210,14 @@ export function ClaudeCodeSettings() { - Enable Bypass Permissions mode + Enable auto-accept permissions - In Bypass Permissions mode, PostHog Code will not ask for your + With auto-accept enabled, PostHog Code will not ask for your approval before running potentially dangerous commands. @@ -227,7 +227,7 @@ export function ClaudeCodeSettings() { By proceeding, you accept all responsibility for actions taken - while running in Bypass Permissions mode. + while auto-accept is enabled. diff --git a/apps/code/src/renderer/features/settings/stores/settingsStore.ts b/apps/code/src/renderer/features/settings/stores/settingsStore.ts index 12162f0cf..984a3b5bc 100644 --- a/apps/code/src/renderer/features/settings/stores/settingsStore.ts +++ b/apps/code/src/renderer/features/settings/stores/settingsStore.ts @@ -26,6 +26,8 @@ interface SettingsStore { lastUsedAdapter: AgentAdapter; lastUsedModel: string | null; lastUsedEnvironments: Record; + lastUsedDirectory: string; + lastUsedCloudRepository: string | null; desktopNotifications: boolean; dockBadgeNotifications: boolean; dockBounceNotifications: boolean; @@ -62,6 +64,8 @@ interface SettingsStore { environmentId: string | null, ) => void; getLastUsedEnvironment: (repoPath: string) => string | null; + setLastUsedDirectory: (directory: string) => void; + setLastUsedCloudRepository: (repository: string | null) => void; setDesktopNotifications: (enabled: boolean) => void; setDockBadgeNotifications: (enabled: boolean) => void; setDockBounceNotifications: (enabled: boolean) => void; @@ -89,6 +93,8 @@ export const useSettingsStore = create()( lastUsedAdapter: "claude", lastUsedModel: null, lastUsedEnvironments: {}, + lastUsedDirectory: "", + lastUsedCloudRepository: null, desktopNotifications: true, dockBadgeNotifications: true, dockBounceNotifications: false, @@ -155,6 +161,10 @@ export const useSettingsStore = create()( }), getLastUsedEnvironment: (repoPath) => get().lastUsedEnvironments[repoPath] ?? null, + setLastUsedDirectory: (directory) => + set({ lastUsedDirectory: directory }), + setLastUsedCloudRepository: (repository) => + set({ lastUsedCloudRepository: repository }), setDesktopNotifications: (enabled) => set({ desktopNotifications: enabled }), setDockBadgeNotifications: (enabled) => @@ -191,6 +201,8 @@ export const useSettingsStore = create()( lastUsedAdapter: state.lastUsedAdapter, lastUsedModel: state.lastUsedModel, lastUsedEnvironments: state.lastUsedEnvironments, + lastUsedDirectory: state.lastUsedDirectory, + lastUsedCloudRepository: state.lastUsedCloudRepository, desktopNotifications: state.desktopNotifications, dockBadgeNotifications: state.dockBadgeNotifications, dockBounceNotifications: state.dockBounceNotifications, diff --git a/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx b/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx index 11055e041..9725ae35d 100644 --- a/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx +++ b/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx @@ -4,7 +4,7 @@ import { ChevronDownIcon } from "@radix-ui/react-icons"; import { Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; import { handleExternalAppAction } from "@utils/handleExternalAppAction"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; const THUMBNAIL_ICON_SIZE = 20; @@ -74,6 +74,8 @@ export function ExternalAppsOpener({ [handleCopyPath], ); + const [dropdownOpen, setDropdownOpen] = useState(false); + if (!targetPath) { return null; } @@ -81,7 +83,17 @@ export function ExternalAppsOpener({ const isReady = !isLoading && detectedApps.length > 0; return ( - + + {dropdownOpen && ( +
+ )}