Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/components/ErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {

render() {
if (this.state.hasError) {
return <ErrorPage error={this.state.error} />;
return (
<ErrorPage
error={this.state.error}
componentStack={this.state.errorInfo?.componentStack ?? undefined}
/>
);
}

return this.props.children;
Expand Down
44 changes: 18 additions & 26 deletions src/engines/BrowserCore/hooks/useBrowserAutomation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
BrowserFrameEvent,
BrowserStatusEvent,
} from "@src/store/workstation/browser/browserAutomationAtom";
import { AsyncUnlistenScope } from "@src/util/platform/tauri/asyncUnlistenScope";
import { invokeTauri, listenTauri } from "@src/util/platform/tauri/init";

/** Options for the useBrowserAutomation hook. */
Expand Down Expand Up @@ -83,42 +84,33 @@ export function useBrowserAutomation(
useEffect(() => {
if (!enabled) return;

let cancelled = false;
const unlisteners: Array<() => void> = [];
const listenerScope = new AsyncUnlistenScope();

const setup = async () => {
try {
const unlistenFrame = await listenTauri<BrowserFrameEvent>(
"browser:frame",
(event) => {
if (!cancelled) {
dispatchFrame(event.payload);
}
}
);
if (!cancelled) unlisteners.push(unlistenFrame);

const unlistenStatus = await listenTauri<BrowserStatusEvent>(
"browser:status",
(event) => {
if (!cancelled) {
dispatchStatus(event.payload);
}
}
);
if (!cancelled) unlisteners.push(unlistenStatus);
await listenerScope.registerAll([
() =>
listenTauri<BrowserFrameEvent>("browser:frame", (event) => {
if (!listenerScope.isDisposed) {
dispatchFrame(event.payload);
}
}),
() =>
listenTauri<BrowserStatusEvent>("browser:status", (event) => {
if (!listenerScope.isDisposed) {
dispatchStatus(event.payload);
}
}),
]);
} catch {
// Tauri events may not be available in web dev mode
}
};

setup();
void setup();

return () => {
cancelled = true;
for (const unlisten of unlisteners) {
unlisten();
}
listenerScope.dispose();
};
}, [enabled, dispatchFrame, dispatchStatus]);

Expand Down
84 changes: 44 additions & 40 deletions src/engines/ChatPanel/panels/ProjectPanelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type { KanbanTask, TaskStatus } from "@src/features/KanbanBoard";
import { allocateCloudAwareWorkItemId } from "@src/features/Org2Cloud/cloudShortId";
import { org2CloudOrgsAtom } from "@src/features/Org2Cloud/org2CloudOrgsAtom";
import { useProjectOrgCloudPermissions } from "@src/features/Org2Cloud/useProjectOrgCloudPermissions";
import { useAsyncResource } from "@src/hooks/async";
import { createLogger } from "@src/hooks/logger";
import {
useCurrentUserMemberIds,
Expand Down Expand Up @@ -105,6 +106,16 @@ interface ProjectPanelViewProps {

const PROJECT_PANEL_TABS: ProjectPanelTab[] = ["overview", "list", "kanban"];

interface ProjectWorkItemsResource {
shortIds: Map<string, string>;
workItems: WorkItem[];
}

const EMPTY_PROJECT_WORK_ITEMS: ProjectWorkItemsResource = {
shortIds: new Map(),
workItems: [],
};

function getProjectOverviewDescription(
project: ChatPanelSelectedProject["project"]
) {
Expand Down Expand Up @@ -136,12 +147,6 @@ export const ProjectPanelView: React.FC<ProjectPanelViewProps> = ({
const [projectBodyLoading, setProjectBodyLoading] = useState(false);
const [projectBodyError, setProjectBodyError] = useState<string | null>(null);
const lastSavedDescriptionRef = useRef(sidebarProjectDescription);
const [workItems, setWorkItems] = useState<WorkItem[]>([]);
const [workItemShortIds, setWorkItemShortIds] = useState<Map<string, string>>(
new Map()
);
const [workItemsLoading, setWorkItemsLoading] = useState(false);
const [workItemsError, setWorkItemsError] = useState<string | null>(null);
const [projectSyncAdapter, setProjectSyncAdapter] = useState<{
projectSlug: string;
adapterId: string | null;
Expand Down Expand Up @@ -365,36 +370,34 @@ export const ProjectPanelView: React.FC<ProjectPanelViewProps> = ({
};
}, [projectSlug, selectedProject.project.id, sidebarProjectDescription]);

const loadProjectWorkItems = useCallback(async () => {
if (!projectSlug) {
setWorkItems([]);
setWorkItemShortIds(new Map());
return;
}

setWorkItemsLoading(true);
setWorkItemsError(null);
try {
const viewData = await projectApi.readWorkItemsViewData(projectSlug, {
view: "list",
});
setWorkItemShortIds(
new Map(viewData.items.map((item) => [item.id, item.shortId]))
const fetchProjectWorkItems = useCallback(
async (scopeProjectSlug: string) => {
const viewData = await projectApi.readWorkItemsViewData(
scopeProjectSlug,
{ view: "list" }
);
setWorkItems(viewData.items.map(enrichedWorkItemToUI));
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to load work items";
logger.error("Failed to load project work items:", error);
setWorkItemsError(message);
} finally {
setWorkItemsLoading(false);
}
}, [projectSlug]);

useEffect(() => {
void loadProjectWorkItems();
}, [loadProjectWorkItems]);
return {
shortIds: new Map(
viewData.items.map((item) => [item.id, item.shortId])
),
workItems: viewData.items.map(enrichedWorkItemToUI),
};
},
[]
);
const workItemsResource = useAsyncResource({
enabled: Boolean(projectSlug),
fetcher: fetchProjectWorkItems,
initialData: EMPTY_PROJECT_WORK_ITEMS,
scopeKey: projectSlug || null,
});
const {
data: { shortIds: workItemShortIds, workItems },
error: workItemsError,
loading: workItemsLoading,
refresh: loadProjectWorkItems,
setData: setWorkItemsData,
} = workItemsResource;

useProjectDataChanged(
useCallback(
Expand Down Expand Up @@ -826,13 +829,14 @@ export const ProjectPanelView: React.FC<ProjectPanelViewProps> = ({
payload
);
const updatedItem = enrichedWorkItemToUI(updated);
setWorkItems((currentItems) =>
currentItems.map((item) =>
setWorkItemsData((current) => ({
...current,
workItems: current.workItems.map((item) =>
item.session_id === workItemId ? updatedItem : item
)
);
),
}));
},
[currentUser, getWorkItemShortId, projectSlug, setWorkItems]
[currentUser, getWorkItemShortId, projectSlug, setWorkItemsData]
);

const handleAddKanbanTask = useCallback(
Expand Down
Loading
Loading