Open files and replace the file row menu with real actions

This commit is contained in:
ZacharyZhang-NY committed 2026-05-28 22:36:31 -04:00
1 parent d2695202b5
commit 69ae525cb7
10 files changed
+81 -10

No files matched your search

@@ -196,6 +196,15 @@ export function registerIpcHandlers(deps: IpcDeps): void {
}
});
ipcMain.handle(IPC_CHANNELS.openFile, async (_event, input) => {
try {
await files.openFile(input);
return ok(null);
} catch (error) {
return fail("file_open_failed", error instanceof Error ? error.message : String(error));
}
});
ipcMain.handle(IPC_CHANNELS.pickDirectory, async () => {
try {
const result = await dialog.showOpenDialog(deps.mainWindow, { properties: ["openDirectory", "createDirectory"] });
@@ -2,8 +2,8 @@ import { createHash } from "node:crypto";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { basename, extname, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { dialog } from "electron";
import type { ImportFileInput } from "../../shared/commands";
import { dialog, shell } from "electron";
import type { ImportFileInput, OpenFileInput } from "../../shared/commands";
import { FILE_SCAN_STATE, type FileScanState } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
@@ -117,6 +117,16 @@ export class FileService {
return id;
}
async openFile(input: OpenFileInput): Promise<void> {
const row = this.db.prepare(
"select storage_path as storagePath, scan_state as scanState from file_objects where id = ? and org_id = ?",
).get(input.fileId, input.orgId) as { storagePath: string; scanState: string } | undefined;
if (!row) throw new Error("File was not found.");
if (row.scanState !== FILE_SCAN_STATE.APPROVED) throw new Error("Blocked files cannot be opened.");
const failure = await shell.openPath(row.storagePath);
if (failure) throw new Error(failure);
}
setPinned(orgId: string, fileId: string, pinned: boolean): void {
const result = this.db.prepare(
"update file_objects set pinned = ?, updated_at = ? where id = ? and org_id = ?",
+1
View File
@@ -43,6 +43,7 @@ const api: DesktopApi = {
createScheduledPost: (input) => invoke(IPC_CHANNELS.createScheduledPost, input),
setScheduledPostState: (input) => invoke(IPC_CHANNELS.setScheduledPostState, input),
importFile: (input) => invoke(IPC_CHANNELS.importFile, input),
openFile: (input) => invoke(IPC_CHANNELS.openFile, input),
pickDirectory: () => invoke(IPC_CHANNELS.pickDirectory),
setFilePinned: (input) => invoke(IPC_CHANNELS.setFilePinned, input),
createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input),
@@ -1,6 +1,7 @@
import { useMemo, useState } from "react";
import { Button, Icon, Segmented } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import type { FileObjectRecord } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId, formatBytes } from "./types";
import { FilesTree } from "./files/FilesTree";
@@ -40,6 +41,13 @@ export function FilesPage({ snapshot, actions }: PageProps) {
if (!result.ok) throw new Error(result.error.message);
});
};
const openFile = (file: FileObjectRecord) => {
void window.agentCompany.openFile({ orgId, fileId: file.id }).then((result) => {
if (!result.ok) throw new Error(result.error.message);
});
};
const togglePin = (file: FileObjectRecord) =>
void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }));
return (
<div style={{ display: "grid", gridTemplateColumns: "minmax(160px, 200px) minmax(0, 1fr)", gap: 24, height: "100%", minHeight: 0, padding: "28px 32px 0" }}>
@@ -83,7 +91,8 @@ export function FilesPage({ snapshot, actions }: PageProps) {
file={file}
ownerName={resolveOwnerName(file, employees, agents)}
grantCount={fileGrantCount(file.id, fileAccessGrants)}
onUnpin={() => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: false }))}
onOpen={() => openFile(file)}
onUnpin={() => togglePin(file)}
/>
))}
</div>
@@ -101,7 +110,8 @@ export function FilesPage({ snapshot, actions }: PageProps) {
grants={fileAccessGrants}
employees={employees}
agents={agents}
onTogglePin={(file) => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
onOpen={openFile}
onTogglePin={togglePin}
/>
) : (
<FilesGrid
@@ -109,7 +119,8 @@ export function FilesPage({ snapshot, actions }: PageProps) {
grants={fileAccessGrants}
employees={employees}
agents={agents}
onTogglePin={(file) => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
onOpen={openFile}
onTogglePin={togglePin}
/>
)}
</div>
@@ -14,12 +14,14 @@ export function FilesGrid({
grants,
employees,
agents,
onOpen,
onTogglePin,
}: {
files: FileObjectRecord[];
grants: FileAccessGrantRecord[];
employees: EmployeeRecord[];
agents: AgentRecord[];
onOpen: (file: FileObjectRecord) => void;
onTogglePin: (file: FileObjectRecord) => void;
}) {
return (
@@ -30,6 +32,7 @@ export function FilesGrid({
file={file}
ownerName={resolveOwnerName(file, employees, agents)}
grantCount={fileGrantCount(file.id, grants)}
onOpen={() => onOpen(file)}
onUnpin={() => onTogglePin(file)}
/>
))}
@@ -41,15 +44,23 @@ export function PinnedCard({
file,
ownerName,
grantCount,
onOpen,
onUnpin,
}: {
file: FileObjectRecord;
ownerName: string;
grantCount: number;
onOpen: () => void;
onUnpin: () => void;
}) {
const canOpen = file.scanState === "approved";
return (
<div
className={canOpen ? "clickable-card" : ""}
role={canOpen ? "button" : undefined}
tabIndex={canOpen ? 0 : undefined}
onClick={canOpen ? onOpen : undefined}
onKeyDown={canOpen ? (event) => { if (event.key === "Enter") onOpen(); } : undefined}
style={{
border: "1px solid var(--border)",
borderRadius: 10,
@@ -59,6 +70,7 @@ export function PinnedCard({
flexDirection: "column",
gap: 10,
minWidth: 0,
cursor: canOpen ? "pointer" : "default",
}}
>
<div className="row" style={{ gap: 10 }}>
@@ -66,7 +78,7 @@ export function PinnedCard({
<button
type="button"
aria-label={file.pinned ? "Unpin" : "Pin"}
onClick={onUnpin}
onClick={(event) => { event.stopPropagation(); onUnpin(); }}
style={{ marginLeft: "auto", padding: 4, border: 0, background: "transparent", color: file.pinned ? "var(--accent)" : "var(--text-4)", cursor: "pointer" }}
>
<Icon name="pin" size={14} />
@@ -1,4 +1,5 @@
import { Avatar, IconButton, Pill } from "../../shared/ui";
import { MenuRow, Popover } from "../../shared/Popover";
import type {
AgentRecord,
EmployeeRecord,
@@ -16,12 +17,14 @@ export function FilesTable({
grants,
employees,
agents,
onOpen,
onTogglePin,
}: {
files: FileObjectRecord[];
grants: FileAccessGrantRecord[];
employees: EmployeeRecord[];
agents: AgentRecord[];
onOpen: (file: FileObjectRecord) => void;
onTogglePin: (file: FileObjectRecord) => void;
}) {
return (
@@ -36,6 +39,7 @@ export function FilesTable({
owner={resolveOwnerName(file, employees, agents)}
grantCount={fileGrantCount(file.id, grants)}
last={index === files.length - 1}
onOpen={() => onOpen(file)}
onTogglePin={() => onTogglePin(file)}
/>
))}
@@ -48,17 +52,24 @@ function FileRow({
owner,
grantCount,
last,
onOpen,
onTogglePin,
}: {
file: FileObjectRecord;
owner: string;
grantCount: number;
last: boolean;
onOpen: () => void;
onTogglePin: () => void;
}) {
const canOpen = file.scanState === "approved";
return (
<div
className="table-row"
className={`table-row ${canOpen ? "clickable" : ""}`.trim()}
role={canOpen ? "button" : undefined}
tabIndex={canOpen ? 0 : undefined}
onClick={canOpen ? onOpen : undefined}
onKeyDown={canOpen ? (event) => { if (event.key === "Enter") onOpen(); } : undefined}
style={{ gridTemplateColumns: ROW_COLUMNS, borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
>
<div className="row" style={{ gap: 10, minWidth: 0 }}>
@@ -77,12 +88,19 @@ function FileRow({
<span style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{owner}</span>
</div>
<div className="row" style={{ gap: 6, minWidth: 0 }}>
<Pill tone={file.scanState === "approved" ? "green" : "red"}>{file.scanState === "approved" ? "Clean" : "Blocked"}</Pill>
<Pill tone={canOpen ? "green" : "red"}>{canOpen ? "Clean" : "Blocked"}</Pill>
<span className="mono" style={{ color: "var(--text-3)", fontSize: 11 }}>{grantCount}</span>
</div>
<div style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(file.updatedAt ?? file.createdAt)}</div>
<div style={{ textAlign: "right" }}>
<IconButton icon={file.pinned ? "pin" : "more"} label={file.pinned ? "Unpin" : "More"} onClick={onTogglePin} />
<div style={{ textAlign: "right" }} onClick={(event) => event.stopPropagation()}>
<Popover trigger={({ toggle }) => <IconButton icon="more" label={`Actions for ${file.name}`} onClick={toggle} />}>
{(close) => (
<>
{canOpen && <MenuRow onClick={() => { onOpen(); close(); }}>Open</MenuRow>}
<MenuRow onClick={() => { onTogglePin(); close(); }}>{file.pinned ? "Unpin" : "Pin"}</MenuRow>
</>
)}
</Popover>
</div>
</div>
);
@@ -519,6 +519,8 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; }
.table-row:last-child { border-bottom: 0; }
.table-row.clickable { cursor: pointer; }
.table-row.clickable:hover { background: var(--hover); }
.clickable-card { transition: border-color 0.1s ease; }
.clickable-card:hover { border-color: var(--border-strong); }
.link-button {
border: 0;
+5
View File
@@ -248,6 +248,11 @@ export interface ImportFileInput {
orgId: string;
}
export interface OpenFileInput {
orgId: string;
fileId: string;
}
export interface CreateMigrationReportInput {
orgId: string;
sourceDatabasePath: string;
@@ -26,6 +26,7 @@ import type {
DeleteOrganizationInput,
EditMessageInput,
ImportFileInput,
OpenFileInput,
ImportSkillDirectoryInput,
GrantRoleInput,
LaunchProviderSignInInput,
@@ -97,6 +98,7 @@ export interface DesktopApi {
createScheduledPost(input: CreateScheduledPostInput): Promise<DesktopResult<AppSnapshot>>;
setScheduledPostState(input: SetScheduledPostStateInput): Promise<DesktopResult<AppSnapshot>>;
importFile(input: ImportFileInput): Promise<DesktopResult<string | null>>;
openFile(input: OpenFileInput): Promise<DesktopResult<null>>;
pickDirectory(): Promise<DesktopResult<string | null>>;
setFilePinned(input: SetFilePinnedInput): Promise<DesktopResult<AppSnapshot>>;
createMigrationReport(input: CreateMigrationReportInput): Promise<DesktopResult<AppSnapshot>>;
+1
View File
@@ -35,6 +35,7 @@ export const IPC_CHANNELS = {
createScheduledPost: "agentcompany:create-scheduled-post",
setScheduledPostState: "agentcompany:set-scheduled-post-state",
importFile: "agentcompany:import-file",
openFile: "agentcompany:open-file",
pickDirectory: "agentcompany:pick-directory",
setFilePinned: "agentcompany:set-file-pinned",
createMigrationReport: "agentcompany:create-migration-report",