improve ai alert copilot ux
This commit is contained in:
parent
ebdc4ab8e6
commit
32e965d782
15 changed files with 931 additions and 15 deletions
305
docs/turns/2026-05-20-ai-alert-copilot-ux.html
Normal file
305
docs/turns/2026-05-20-ai-alert-copilot-ux.html
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>2026-05-20 · AI Alert Copilot UX</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: oklch(0.11 0.01 250);
|
||||
--panel: oklch(0.16 0.013 250 / 0.94);
|
||||
--panel-2: oklch(0.13 0.012 250 / 0.92);
|
||||
--text: oklch(0.93 0.014 250);
|
||||
--muted: oklch(0.74 0.018 250);
|
||||
--faint: oklch(0.6 0.016 250);
|
||||
--line: oklch(0.75 0.014 250 / 0.14);
|
||||
--accent: oklch(0.79 0.12 74);
|
||||
--accent-soft: oklch(0.79 0.12 74 / 0.12);
|
||||
--green-soft: oklch(0.75 0.12 151 / 0.12);
|
||||
--red-soft: oklch(0.72 0.14 28 / 0.12);
|
||||
--shadow: 0 24px 80px oklch(0.02 0.01 250 / 0.42);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top left, oklch(0.8 0.12 74 / 0.08), transparent 30%),
|
||||
linear-gradient(180deg, oklch(0.15 0.012 250), var(--bg));
|
||||
color: var(--text);
|
||||
}
|
||||
main {
|
||||
width: min(1160px, calc(100vw - 40px));
|
||||
margin: 0 auto;
|
||||
padding: 36px 0 64px;
|
||||
}
|
||||
h1, h2, h3, .eyebrow, .meta, code, pre {
|
||||
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||||
}
|
||||
h1, h2, h3 { margin: 0; }
|
||||
p, li { color: var(--muted); line-height: 1.7; }
|
||||
a { color: var(--accent); }
|
||||
main > * + * { margin-top: 22px; }
|
||||
.hero, section {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.hero {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
padding: 28px;
|
||||
background: linear-gradient(135deg, oklch(0.2 0.017 250 / 0.96), oklch(0.14 0.012 250 / 0.98));
|
||||
}
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: clamp(2rem, 3.4vw, 3.5rem);
|
||||
line-height: 0.96;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.hero-copy { max-width: 76ch; margin: 14px 0 0; }
|
||||
.hero-grid, .two-col, .validation-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
.hero-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.two-col { grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr); }
|
||||
.validation-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.stat, .validation-card, .callout {
|
||||
padding: 16px 18px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: oklch(0.12 0.01 250 / 0.5);
|
||||
}
|
||||
.stat span, .validation-card span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
.validation-card.good { background: var(--green-soft); }
|
||||
.validation-card.warn { background: var(--red-soft); }
|
||||
.callout { background: var(--accent-soft); }
|
||||
section {
|
||||
padding: 24px;
|
||||
background: var(--panel-2);
|
||||
}
|
||||
section h2 {
|
||||
margin-bottom: 14px;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
ul { margin: 0; padding-left: 20px; }
|
||||
li + li { margin-top: 10px; }
|
||||
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid oklch(0.8 0.12 74 / 0.28);
|
||||
background: var(--accent-soft);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.meta { color: var(--faint); font-size: 0.82rem; }
|
||||
pre.diff {
|
||||
margin: 0;
|
||||
padding: 16px 18px;
|
||||
overflow: auto;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: oklch(0.08 0.008 250 / 0.95);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.diff-title {
|
||||
margin: 18px 0 10px;
|
||||
color: var(--faint);
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
main { width: min(100vw - 24px, 1160px); padding-top: 18px; }
|
||||
.hero-grid, .two-col, .validation-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<article class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">Repository Turn Document</p>
|
||||
<h1>AI Alert Copilot UX</h1>
|
||||
<p class="hero-copy">
|
||||
Upgraded the desktop Copilot result surface so generated analysis now reads like Islandflow content instead of raw terminal output. The alert panel can reuse session results by action, expose Regenerate after completion, show live running/cancel states, and request task cancellation through the desktop bridge.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-grid">
|
||||
<div class="stat"><span>Issue</span><strong>islandflow-xtg</strong></div>
|
||||
<div class="stat"><span>Primary Surface</span><strong>Alert Copilot</strong></div>
|
||||
<div class="stat"><span>Validation</span><strong>272 tests passed</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<section>
|
||||
<h2>Summary</h2>
|
||||
<p>
|
||||
Implemented the AI alert Copilot UX refinement: markdown rendering, in-place loading and cancellation, current-session task caching for alert actions, and regenerate controls for completed outputs.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="two-col">
|
||||
<div>
|
||||
<h2>Changes Made</h2>
|
||||
<ul>
|
||||
<li>Added <code>react-markdown</code> to the web app and replaced raw Copilot result <code>pre</code> output with safe markdown rendering.</li>
|
||||
<li>Created reusable task-surface helpers for empty, running, completed, failed, and cancelled display decisions.</li>
|
||||
<li>Added a compact spinner, status copy, and Cancel button for queued/running task states.</li>
|
||||
<li>Added per-session alert task caching keyed by action kind and smart-money event context.</li>
|
||||
<li>Added Regenerate for completed alert, replay, and screen compiler results.</li>
|
||||
<li>Extended the desktop AI bridge, preload script, IPC constants, main process handler, and desktop service with <code>cancelTask(taskId)</code>.</li>
|
||||
<li>Added tests for cache selection, regenerate eligibility, task-surface states, and desktop cancellation behavior.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Context</h2>
|
||||
<p>
|
||||
The previous Copilot output surface showed model text as raw preformatted content. That made structured markdown harder to scan and gave users no way to interrupt a task once it started.
|
||||
</p>
|
||||
<p>
|
||||
The plan treated persistence as current app-session persistence only, so the cache intentionally lives in panel state rather than disk or a global store.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Important Implementation Details</h2>
|
||||
<ul>
|
||||
<li>Markdown is rendered through <code>ReactMarkdown</code> without raw HTML support, so embedded HTML is not executed as markup.</li>
|
||||
<li>Alert action cache keys combine <code>IslandflowAiTaskKind</code> with <code>underlying_id</code>, <code>source_ts</code>, and <code>event_id</code>.</li>
|
||||
<li>Completed cached alert actions select the existing task instead of starting a new run. Regenerate intentionally replaces the cached task id for that action.</li>
|
||||
<li>Cancellation marks the task <code>cancelled</code> locally, removes its active thread mapping, ignores later deltas/completion, and best-effort calls <code>turn/cancel</code> on the app-server.</li>
|
||||
<li>Replay and screen compiler panels reuse the improved result surface, cancellation, and regenerate wording where they already share task output behavior.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Relevant Diff Snippets</h2>
|
||||
<p class="meta">
|
||||
Patch-style snippets prepared for Diffs-style review. Reference: <a href="https://diffs.com/docs">Diffs rendering docs</a>.
|
||||
</p>
|
||||
<p class="diff-title">Task result surface</p>
|
||||
<pre class="diff" data-diffs="patch"><code>@@ apps/web/app/desktop-ai-panels.tsx
|
||||
+import ReactMarkdown from "react-markdown";
|
||||
+
|
||||
+export const getCopilotTaskSurfaceState = (...) => {
|
||||
+ if (!task) return "empty";
|
||||
+ if (RUNNING_TASK_STATUSES.includes(task.status)) return "running";
|
||||
+ if (task.status === "completed") return "completed";
|
||||
+ if (task.status === "failed") return "failed";
|
||||
+ return "cancelled";
|
||||
+};
|
||||
+
|
||||
+{surfaceState === "running" ? (
|
||||
+ <div className="copilot-task-running-row">
|
||||
+ <span className="copilot-spinner" aria-hidden="true" />
|
||||
+ <p className="copilot-note">Generating analysis as deltas arrive.</p>
|
||||
+ <button className="terminal-button" onClick={() => onCancel(task.taskId)}>Cancel</button>
|
||||
+ </div>
|
||||
+) : null}
|
||||
+{task.text ? (
|
||||
+ <div className="copilot-markdown">
|
||||
+ <ReactMarkdown>{task.text}</ReactMarkdown>
|
||||
+ </div>
|
||||
+) : null}</code></pre>
|
||||
|
||||
<p class="diff-title">Desktop cancellation bridge</p>
|
||||
<pre class="diff" data-diffs="patch"><code>@@ apps/desktop/src/desktop-ai-ipc.ts
|
||||
+export const DESKTOP_AI_CANCEL_TASK = "islandflow:desktop-ai:cancel-task";
|
||||
|
||||
@@ apps/desktop/src/main.ts
|
||||
+ipcMain.handle(DESKTOP_AI_CANCEL_TASK, async (event, taskId) => {
|
||||
+ guard(event);
|
||||
+ await service.cancelTask(String(taskId));
|
||||
+});
|
||||
|
||||
@@ apps/desktop/src/desktop-ai.ts
|
||||
+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 });
|
||||
+ if (task.threadId) {
|
||||
+ this.activeTasksByThreadId.delete(task.threadId);
|
||||
+ await this.client.request("turn/cancel", { threadId: task.threadId, turnId: task.turnId ?? undefined }).catch(() => undefined);
|
||||
+ }
|
||||
+}</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Expected Impact for End-Users</h2>
|
||||
<ul>
|
||||
<li>Generated Copilot analysis is easier to read because headings, lists, code, and inline emphasis render with Islandflow typography.</li>
|
||||
<li>Users get immediate feedback while a task is queued or running, instead of an empty-looking panel.</li>
|
||||
<li>Users can cancel a running Copilot task from the same surface where the output appears.</li>
|
||||
<li>Switching between alert AI actions no longer discards completed outputs during the same app session.</li>
|
||||
<li>Regenerate makes fresh model runs deliberate rather than accidental.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Validation</h2>
|
||||
<div class="validation-grid">
|
||||
<div class="validation-card good">
|
||||
<span>Passed</span>
|
||||
<strong><code>bun test</code></strong>
|
||||
<p>All 272 tests passed across 43 files.</p>
|
||||
</div>
|
||||
<div class="validation-card good">
|
||||
<span>Passed</span>
|
||||
<strong><code>bun test apps/web/app/desktop-ai.test.ts apps/desktop/src/desktop-ai.test.ts</code></strong>
|
||||
<p>23 focused desktop AI tests passed after the final helper adjustment.</p>
|
||||
</div>
|
||||
<div class="validation-card warn">
|
||||
<span>Blocked</span>
|
||||
<strong><code>bun --cwd=apps/web run build</code></strong>
|
||||
<p>The production build compiled successfully, then failed during TypeScript on the known <code>packages/types/src/desktop-ai.ts</code> import-extension issue: <code>./events.ts</code> requires <code>allowImportingTsExtensions</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Issues, Limitations, and Mitigations</h2>
|
||||
<ul>
|
||||
<li>The app-server cancellation protocol is treated as best-effort. If <code>turn/cancel</code> is unsupported, Islandflow still marks the task cancelled locally and ignores late output.</li>
|
||||
<li>Session result caching is intentionally in-memory panel state only. It does not survive a desktop restart or full page reload.</li>
|
||||
<li>The web build remains blocked by the existing shared-types TypeScript extension configuration issue, not by this Copilot UX implementation.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Follow-up Work</h2>
|
||||
<ul>
|
||||
<li>Consider a dedicated Beads issue to resolve the shared-types <code>.ts</code> import extension build blocker.</li>
|
||||
<li>Consider moving task caches into a small shared hook if replay and screen compiler need context-key reuse beyond the active result.</li>
|
||||
<li>Consider adding visual browser coverage for markdown-heavy Copilot output once a stable desktop/web test harness is available.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue