improve ai alert copilot ux
This commit is contained in:
parent
ebdc4ab8e6
commit
32e965d782
15 changed files with 931 additions and 15 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue