improve ai alert copilot ux

This commit is contained in:
dirtydishes 2026-05-20 19:38:17 -04:00
parent ebdc4ab8e6
commit 32e965d782
15 changed files with 931 additions and 15 deletions

View file

@ -6,3 +6,4 @@ export const DESKTOP_AI_CANCEL_LOGIN = "islandflow:desktop-ai:cancel-login";
export const DESKTOP_AI_LOGOUT = "islandflow:desktop-ai:logout";
export const DESKTOP_AI_UPDATE_PREFERENCES = "islandflow:desktop-ai:update-preferences";
export const DESKTOP_AI_RUN_TASK = "islandflow:desktop-ai:run-task";
export const DESKTOP_AI_CANCEL_TASK = "islandflow:desktop-ai:cancel-task";

View file

@ -171,4 +171,65 @@ describe("desktop ai usage and state tracking", () => {
expect(service.getState().account.planType).toBeNull();
expect(service.getState().account.login).toEqual({ status: "idle", message: "Logged out." });
});
it("marks active tasks cancelled and ignores later deltas or completion", async () => {
const dir = await makeTempDir();
const service = new IslandflowDesktopAiService(dir, async () => {}, () => {});
const internal = service as any;
const requests: Array<{ method: string; params: unknown }> = [];
internal.client = {
request: async (method: string, params: unknown) => {
requests.push({ method, params });
return {};
}
};
internal.state.tasks = [
{
taskId: "task-1",
kind: "smart-money-explain",
title: "Explain smart money event",
subtitle: "AAPL",
status: "running",
createdAt: Date.now(),
updatedAt: Date.now(),
threadId: "thread-1",
turnId: "turn-1",
model: "gpt-5.4",
reasoningEffort: "high",
text: "partial",
error: null,
compiledScreen: null
}
];
internal.activeTasksByThreadId.set("thread-1", {
taskId: "task-1",
taskKind: "smart-money-explain",
taskTitle: "Explain smart money event",
profileId: "managed-chatgpt"
});
await service.cancelTask("task-1");
await internal.handleNotification("item/agentMessage/delta", {
threadId: "thread-1",
delta: " late delta"
});
await internal.handleNotification("turn/completed", {
threadId: "thread-1",
turn: {
id: "turn-1",
status: "completed",
error: null
}
});
expect(requests).toEqual([
{
method: "turn/cancel",
params: { threadId: "thread-1", turnId: "turn-1" }
}
]);
expect(service.getState().tasks[0]?.status).toBe("cancelled");
expect(service.getState().tasks[0]?.text).toBe("partial");
});
});

View file

@ -650,6 +650,7 @@ export class IslandflowDesktopAiService {
private readonly sandboxCwd: string;
private readonly client: CodexAppServerClient;
private readonly activeTasksByThreadId = new Map<string, ActiveTaskContext>();
private readonly locallyCancelledTaskIds = new Set<string>();
private usageStore: PersistedUsageStore = createUsageStore();
private state: IslandflowAiState = createInitialState();
private serviceTier: string | null = null;
@ -851,6 +852,32 @@ export class IslandflowDesktopAiService {
}
}
async cancelTask(taskId: string): Promise<void> {
const task = this.state.tasks.find((candidate) => candidate.taskId === taskId);
if (!task || (task.status !== "queued" && task.status !== "running")) {
return;
}
this.locallyCancelledTaskIds.add(taskId);
this.patchTask(taskId, {
status: "cancelled",
error: null
});
const threadId = task.threadId;
if (!threadId) {
return;
}
this.activeTasksByThreadId.delete(threadId);
const params = {
threadId,
turnId: task.turnId ?? undefined
};
await this.client.request("turn/cancel", params).catch(() => undefined);
}
private async ensureClientReady(): Promise<void> {
const selectedProfile = this.resolveSelectedProfileMode();
this.state.transportStatus = this.state.transportStatus === "restarting" ? "restarting" : "starting";
@ -979,6 +1006,9 @@ export class IslandflowDesktopAiService {
if (!activeTask) {
return;
}
if (this.locallyCancelledTaskIds.has(activeTask.taskId)) {
return;
}
const current = this.state.tasks.find((task) => task.taskId === activeTask.taskId);
if (!current) {
return;
@ -1000,6 +1030,9 @@ export class IslandflowDesktopAiService {
if (!activeTask) {
return;
}
if (this.locallyCancelledTaskIds.has(activeTask.taskId)) {
return;
}
if (typeof payload.item.text === "string") {
this.patchTask(activeTask.taskId, {
text: payload.item.text
@ -1032,6 +1065,10 @@ export class IslandflowDesktopAiService {
if (!activeTask) {
return;
}
if (this.locallyCancelledTaskIds.has(activeTask.taskId)) {
this.activeTasksByThreadId.delete(payload.threadId);
return;
}
const current = this.state.tasks.find((task) => task.taskId === activeTask.taskId);
if (!current) {
return;

View file

@ -11,6 +11,7 @@ import {
import { IslandflowDesktopAiService } from "./desktop-ai.js";
import {
DESKTOP_AI_CANCEL_LOGIN,
DESKTOP_AI_CANCEL_TASK,
DESKTOP_AI_GET_STATE,
DESKTOP_AI_LOGIN_BROWSER,
DESKTOP_AI_LOGIN_DEVICE,
@ -168,6 +169,11 @@ const registerDesktopAiIpc = (service: IslandflowDesktopAiService): void => {
guard(event);
return service.runTask(request);
});
ipcMain.handle(DESKTOP_AI_CANCEL_TASK, async (event, taskId) => {
guard(event);
await service.cancelTask(String(taskId));
});
};
const ensureMainWindow = (): void => {

View file

@ -8,6 +8,7 @@ const DESKTOP_AI_CANCEL_LOGIN = "islandflow:desktop-ai:cancel-login";
const DESKTOP_AI_LOGOUT = "islandflow:desktop-ai:logout";
const DESKTOP_AI_UPDATE_PREFERENCES = "islandflow:desktop-ai:update-preferences";
const DESKTOP_AI_RUN_TASK = "islandflow:desktop-ai:run-task";
const DESKTOP_AI_CANCEL_TASK = "islandflow:desktop-ai:cancel-task";
type DesktopAiState = any;
type DesktopAiTaskRequest = any;
@ -27,6 +28,8 @@ const bridge = {
ipcRenderer.invoke(DESKTOP_AI_UPDATE_PREFERENCES, next),
runTask: (request: DesktopAiTaskRequest): Promise<{ taskId: string }> =>
ipcRenderer.invoke(DESKTOP_AI_RUN_TASK, request),
cancelTask: (taskId: string): Promise<void> =>
ipcRenderer.invoke(DESKTOP_AI_CANCEL_TASK, taskId),
subscribe: (listener: (state: DesktopAiState) => void): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, state: DesktopAiState) => {
listener(state);