Skip to content
Merged
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 changes: 3 additions & 0 deletions NEXT_RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
---
type: patch
---

- Improved agent startup behavior, including preferred-agent auto-launch and clearer handling when an installed agent version isn't supported.
- Improved Markdown workspace startup and file navigation reliability.
1 change: 1 addition & 0 deletions e2e/files-click-tour.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ test("deleting the active file closes the central file view", async ({
).toBeVisible();

await file.click();
await file.focus();
await page.keyboard.press(deleteShortcut);

await expect(file).toHaveCount(0);
Expand Down
7 changes: 6 additions & 1 deletion electron/agent-status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,15 @@ export function choosePreferredAgent(agents) {
const preferredAgent = candidates.includes(tier.preferredTie)
? tier.preferredTie
: candidates[0];
const supportsAutoLaunch =
tier.autoLaunch && agents[preferredAgent]?.supportedVersion === true;
return {
agents,
autoLaunchAgent: tier.autoLaunch ? preferredAgent : null,
autoLaunchAgent: supportsAutoLaunch ? preferredAgent : null,
preferredAgent,
reason: tier.reason,
versionBlockedAutoLaunchAgent:
tier.autoLaunch && !supportsAutoLaunch ? preferredAgent : null,
};
}

Expand All @@ -125,6 +129,7 @@ export function choosePreferredAgent(agents) {
autoLaunchAgent: null,
preferredAgent: "claude",
reason: "fallback",
versionBlockedAutoLaunchAgent: null,
};
}

Expand Down
46 changes: 42 additions & 4 deletions electron/agent-status.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,23 +78,25 @@ describe("agent preference ranking", () => {
test("uses requested tier tie-breaks and auto-launch rules", () => {
expect(
choosePreferredAgent({
claude: status({ authStatus: "paid" }),
codex: status({ authStatus: "paid" }),
claude: status({ authStatus: "paid", supportedVersion: true }),
codex: status({ authStatus: "paid", supportedVersion: true }),
}),
).toMatchObject({
autoLaunchAgent: "claude",
preferredAgent: "claude",
reason: "paid",
versionBlockedAutoLaunchAgent: null,
});
expect(
choosePreferredAgent({
claude: status({ authStatus: "free" }),
codex: status({ authStatus: "free" }),
claude: status({ authStatus: "free", supportedVersion: true }),
codex: status({ authStatus: "free", supportedVersion: true }),
}),
).toMatchObject({
autoLaunchAgent: "codex",
preferredAgent: "codex",
reason: "free",
versionBlockedAutoLaunchAgent: null,
});
expect(
choosePreferredAgent({
Expand Down Expand Up @@ -127,6 +129,42 @@ describe("agent preference ranking", () => {
reason: "installed",
});
});

test("blocks paid and free auto-launches when the selected agent version is unsupported", () => {
expect(
choosePreferredAgent({
claude: status({ authStatus: "paid", supportedVersion: false }),
codex: status({ authStatus: "paid", supportedVersion: true }),
}),
).toMatchObject({
autoLaunchAgent: null,
preferredAgent: "claude",
reason: "paid",
versionBlockedAutoLaunchAgent: "claude",
});
expect(
choosePreferredAgent({
claude: status({ authStatus: "free", supportedVersion: true }),
codex: status({ authStatus: "free", supportedVersion: false }),
}),
).toMatchObject({
autoLaunchAgent: null,
preferredAgent: "codex",
reason: "free",
versionBlockedAutoLaunchAgent: "codex",
});
expect(
choosePreferredAgent({
claude: status({ authStatus: "paid", supportedVersion: false }),
codex: status({ authStatus: "free", supportedVersion: true }),
}),
).toMatchObject({
autoLaunchAgent: null,
preferredAgent: "claude",
reason: "paid",
versionBlockedAutoLaunchAgent: "claude",
});
});
});

describe("agent preference probe", () => {
Expand Down
1 change: 1 addition & 0 deletions electron/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export type DesktopAgentPreferencePayload =
export type DesktopAgentPreferenceResult = {
preferredAgent: DesktopAgentName;
autoLaunchAgent: DesktopAgentName | null;
versionBlockedAutoLaunchAgent: DesktopAgentName | null;
reason: DesktopAgentPreferenceReason;
agents: Record<DesktopAgentName, DesktopAgentStatus>;
};
Expand Down
2 changes: 2 additions & 0 deletions src/extension-runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export interface ExtensionContext {
readonly panel?: PanelSide;
readonly fileId: string;
}) => void;
/** File id for the file currently active in the central editor panel. */
readonly activeFileId?: string | null;
/** Path for the file currently active in the central editor panel. */
readonly activeFilePath?: string | null;
readonly isPanelFocused?: boolean;
Expand Down
78 changes: 74 additions & 4 deletions src/extensions/files/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,71 @@ describe("FilesView", () => {
await lix.close();
});

test("Cmd+Backspace deletes the active file id, not the active path", async () => {
const lix = await openLix();
const closeFileViews = vi.fn();
await qb(lix)
.insertInto("lix_file")
.values([
{
id: "file_viewed",
path: "/viewed.md",
data: new Uint8Array(),
},
{
id: "file_at_path",
path: "/path-active.md",
data: new Uint8Array(),
},
])
.execute();

let utils: ReturnType<typeof render>;
await act(async () => {
utils = render(
<LixProvider lix={lix}>
<Suspense fallback={null}>
<FilesView
context={createViewContext(lix, {
activeFileId: "file_viewed",
activeFilePath: "/path-active.md",
closeFileViews,
})}
/>
</Suspense>
</LixProvider>,
);
});

await waitFor(() => {
expect(queryTreeItemByLabel(utils!, "viewed.md")).toHaveAttribute(
"data-item-selected",
"true",
);
});

await act(async () => {
fireEvent.keyDown(document, { key: "Backspace", metaKey: true });
});

await waitFor(async () => {
const rows = await qb(lix)
.selectFrom("lix_file")
.select(["id", "path"])
.execute();
expect(rows.some((row) => row.id === "file_viewed")).toBe(false);
expect(rows).toContainEqual({
id: "file_at_path",
path: "/path-active.md",
});
});

expect(closeFileViews).toHaveBeenCalledWith({ fileId: "file_viewed" });

utils!.unmount();
await lix.close();
});

test("Cmd+Backspace in an empty editor keeps a newly created selected file and editor event", async () => {
const lix = await openLix();
const openFile = vi.fn();
Expand Down Expand Up @@ -996,7 +1061,12 @@ describe("FilesView", () => {
.values({ path: "/docs/" } as any)
.execute();
await writeReviewFile(lix, "file_live", "/docs/live.md", "after");
await writeReviewFile(lix, "file_live_only", "/docs/live-only.md", "head");
await writeReviewFile(
lix,
"file_live_only",
"/docs/live-only.md",
"head",
);

let utils: ReturnType<typeof render>;
await act(async () => {
Expand Down Expand Up @@ -1064,9 +1134,9 @@ describe("FilesView", () => {
"data-item-git-status",
"recreated",
);
expect(queryTreeItemByLabel(utils!, "unchanged.md")).not.toHaveAttribute(
"data-item-git-status",
);
expect(
queryTreeItemByLabel(utils!, "unchanged.md"),
).not.toHaveAttribute("data-item-git-status");
expect(queryTreeItemByLabel(utils!, "live-only.md")).toBeNull();
});

Expand Down
56 changes: 34 additions & 22 deletions src/extensions/files/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function FilesViewContent({
const [selectedSource, setSelectedSource] =
useState<FilesystemTreeSource | null>(null);
const selectedKindRef = useRef(selectedKind);
const syncedActiveFilePathRef = useRef<string | null>(null);
const syncedActiveFileSelectionRef = useRef<string | null>(null);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const dragCounterRef = useRef(0);
const entryPathSet = useMemo(() => {
Expand Down Expand Up @@ -164,6 +164,10 @@ function FilesViewContent({
}
return combined;
}, [entryDirectorySet, pendingDirectoryPaths]);
const activeFileId =
typeof context?.activeFileId === "string" && context.activeFileId.length > 0
? context.activeFileId
: null;
const activeFilePath = context?.activeFilePath ?? null;
useEffect(() => {
if (pendingPaths.length === 0) return;
Expand All @@ -180,13 +184,18 @@ function FilesViewContent({
}, [selectedKind]);
useEffect(() => {
if (createRequest) return;
const activeIdentity = activeFileId
? `id:${activeFileId}`
: activeFilePath
? `path:${normalizeFilePath(activeFilePath)}`
: null;
const normalizedActiveFilePath =
typeof activeFilePath === "string" && activeFilePath.length > 0
? normalizeFilePath(activeFilePath)
: null;

if (!normalizedActiveFilePath) {
syncedActiveFilePathRef.current = null;
if (!activeIdentity) {
syncedActiveFileSelectionRef.current = null;
if (selectedKindRef.current === "file") {
setSelectedPath(null);
setSelectedFileId(null);
Expand All @@ -197,15 +206,15 @@ function FilesViewContent({
return;
}

if (syncedActiveFilePathRef.current === normalizedActiveFilePath) {
return;
}

const activeEntry = combinedEntries.find(
(entry) =>
entry.kind === "file" &&
filesystemEntryPathKey(entry) === normalizedActiveFilePath,
);
const activeEntry = activeFileId
? combinedEntries.find(
(entry) => entry.kind === "file" && entry.id === activeFileId,
)
: combinedEntries.find(
(entry) =>
entry.kind === "file" &&
filesystemEntryPathKey(entry) === normalizedActiveFilePath,
);

if (!activeEntry) {
if (selectedKindRef.current === "file") {
Expand All @@ -218,16 +227,20 @@ function FilesViewContent({
return;
}

syncedActiveFilePathRef.current = normalizedActiveFilePath;
const activeEntryPath = filesystemEntryPathKey(activeEntry);
const activeSelectionKey = `${activeIdentity}:${activeEntryPath}`;
if (syncedActiveFileSelectionRef.current === activeSelectionKey) {
return;
}

syncedActiveFileSelectionRef.current = activeSelectionKey;
selectedKindRef.current = "file";
setSelectedPath(normalizedActiveFilePath);
setSelectedPath(activeEntryPath);
setSelectedFileId(activeEntry.id);
setSelectedKind("file");
setSelectedSource(activeEntry.source ?? "lix");
setOpenDirectoryPaths((prev) => {
const ancestors = ancestorDirectoryPathsForFilePath(
normalizedActiveFilePath,
);
const ancestors = ancestorDirectoryPathsForFilePath(activeEntryPath);
if (ancestors.length === 0) return prev;
const next = new Set(prev);
let changed = false;
Expand All @@ -239,7 +252,7 @@ function FilesViewContent({
}
return changed ? next : prev;
});
}, [activeFilePath, combinedEntries, createRequest]);
}, [activeFileId, activeFilePath, combinedEntries, createRequest]);
const isMacPlatform = useMemo(() => detectMacPlatform(), []);
const isPanelFocused = context?.isPanelFocused ?? false;
const registerNewFileDraftHandler = context?.registerNewFileDraftHandler;
Expand Down Expand Up @@ -662,16 +675,15 @@ function FilesViewContent({
: ensureDirectoryPath(selectedPath);
try {
if (selectedKind === "file") {
if (!selectedFileId) return;
await qb(lix)
.deleteFrom("lix_file")
.where("path", "=", normalizedPath)
.where("id", "=", selectedFileId)
.execute();
setPendingPaths((prev) =>
prev.filter((path) => path !== normalizedPath),
);
if (selectedFileId) {
context?.closeFileViews?.({ fileId: selectedFileId });
}
context?.closeFileViews?.({ fileId: selectedFileId });
} else {
await qb(lix)
.deleteFrom("lix_directory")
Expand Down
1 change: 1 addition & 0 deletions src/extensions/terminal/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ function createTerminalApi(): DesktopTerminalApi {
getPreferredAgent: vi.fn().mockResolvedValue({
preferredAgent: "claude",
autoLaunchAgent: null,
versionBlockedAutoLaunchAgent: null,
reason: "fallback",
agents: {
claude: {
Expand Down
Loading
Loading