@xenide-io/the-old-ui-theme 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Chip-DKT-82Lf.d.mts → Chip-Dw2g2t8D.d.mts} +14 -2
- package/dist/{Chip-DKT-82Lf.d.ts → Chip-Dw2g2t8D.d.ts} +14 -2
- package/dist/{chunk-LHE4ROON.mjs → chunk-RIPGN6FG.mjs} +412 -61
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +355 -23
- package/dist/index.mjs +11 -3
- package/dist/suite-analytics--Mf1RZbw.d.mts +29 -0
- package/dist/suite-analytics--Mf1RZbw.d.ts +29 -0
- package/dist/suite.d.mts +14 -4
- package/dist/suite.d.ts +14 -4
- package/dist/suite.js +920 -513
- package/dist/suite.mjs +898 -523
- package/dist/ui.d.mts +2 -2
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +401 -66
- package/dist/ui.mjs +3 -1
- package/package.json +1 -1
- package/src/app/globals.css +57 -1
- package/src/components/ui/BrandSpinner.tsx +256 -0
- package/src/components/ui/CodeBlock.tsx +11 -1
- package/src/components/ui/Marketing.tsx +17 -1
- package/src/components/ui/SettingsLayout.tsx +4 -0
- package/src/components/ui/Table.tsx +32 -9
- package/src/components/ui/brand-spinner.test.tsx +62 -0
- package/src/components/ui/index.ts +7 -0
- package/src/components/ui/table.test.tsx +31 -4
- package/src/styles/suite-skin.css +78 -46
- package/src/suite/components/ai-draft-editor.tsx +184 -0
- package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
- package/src/suite/components/ai-panel.tsx +184 -14
- package/src/suite/components/app-switcher.tsx +13 -0
- package/src/suite/components/command-palette-host.tsx +20 -2
- package/src/suite/components/suite-notification-bell.tsx +23 -2
- package/src/suite/components/suite-notification-settings.tsx +11 -0
- package/src/suite/components/suite-settings-layout.tsx +1 -1
- package/src/suite/components/suite-settings-mobile-nav.tsx +5 -1
- package/src/suite/components/suite-sidebar.tsx +2 -2
- package/src/suite/components/theme-provider.tsx +2 -0
- package/src/suite/components/today-ui.tsx +33 -28
- package/src/suite/index.ts +8 -0
- package/src/suite/lib/suite-notifications.tsx +5 -0
|
@@ -62,20 +62,56 @@ body {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/* Shared app canvas. Dense workspaces do not paint a decorative background. */
|
|
65
|
-
/* One centred marketing header and one button face across the product family.
|
|
65
|
+
/* One centred marketing header and one button face across the product family.
|
|
66
|
+
The header stays transparent; scrolling content blurs and fades behind it,
|
|
67
|
+
the same treatment as the Today title and the blog/guide chrome. */
|
|
68
|
+
.marketing-header,
|
|
69
|
+
.landing-page-nav {
|
|
70
|
+
isolation: isolate;
|
|
71
|
+
background: transparent;
|
|
72
|
+
border-bottom: none;
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
.marketing-header {
|
|
67
76
|
position: sticky;
|
|
68
77
|
top: 0;
|
|
69
78
|
z-index: 50;
|
|
70
|
-
border-bottom: 1px solid var(--ph-border-subtle);
|
|
71
|
-
background: color-mix(in oklab, var(--ph-canvas) 88%, transparent);
|
|
72
|
-
-webkit-backdrop-filter: blur(16px) saturate(1.15);
|
|
73
|
-
backdrop-filter: blur(16px) saturate(1.15);
|
|
74
79
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
|
|
81
|
+
.marketing-header::before,
|
|
82
|
+
.landing-page-nav::before {
|
|
83
|
+
content: "";
|
|
84
|
+
position: absolute;
|
|
85
|
+
inset-inline: 0;
|
|
86
|
+
top: 0;
|
|
87
|
+
height: calc(100% + var(--suite-scroll-fade));
|
|
88
|
+
z-index: -1;
|
|
89
|
+
/* No surface fill: the page background shows through from the very top,
|
|
90
|
+
and only passing content is blurred. Keeps the header from reading as a
|
|
91
|
+
lighter bar. */
|
|
92
|
+
background: transparent;
|
|
93
|
+
-webkit-backdrop-filter: blur(8px);
|
|
94
|
+
backdrop-filter: blur(8px);
|
|
95
|
+
-webkit-mask-image: linear-gradient(
|
|
96
|
+
to bottom,
|
|
97
|
+
#000 calc(100% - var(--suite-scroll-fade)),
|
|
98
|
+
transparent
|
|
99
|
+
);
|
|
100
|
+
mask-image: linear-gradient(
|
|
101
|
+
to bottom,
|
|
102
|
+
#000 calc(100% - var(--suite-scroll-fade)),
|
|
103
|
+
transparent
|
|
104
|
+
);
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* At rest the header is fully transparent so the page background starts at the
|
|
109
|
+
top. The frost only kicks in once content scrolls beneath it. */
|
|
110
|
+
.marketing-header[data-scrolled="false"]::before {
|
|
111
|
+
-webkit-backdrop-filter: none;
|
|
112
|
+
backdrop-filter: none;
|
|
113
|
+
-webkit-mask-image: none;
|
|
114
|
+
mask-image: none;
|
|
79
115
|
}
|
|
80
116
|
.marketing-header__inner {
|
|
81
117
|
display: flex;
|
|
@@ -269,8 +305,10 @@ body {
|
|
|
269
305
|
.marketing-landing::before {
|
|
270
306
|
content: "";
|
|
271
307
|
position: absolute;
|
|
272
|
-
|
|
273
|
-
|
|
308
|
+
/* Start at the very top so the atmosphere is behind the header too — the
|
|
309
|
+
header is transparent, so an inset here would read as a bare strip. */
|
|
310
|
+
inset: 0 0 auto;
|
|
311
|
+
height: 75rem;
|
|
274
312
|
z-index: -1;
|
|
275
313
|
pointer-events: none;
|
|
276
314
|
background:
|
|
@@ -458,7 +496,7 @@ html[data-marketing-paused] .marketing-landing::before {
|
|
|
458
496
|
}
|
|
459
497
|
|
|
460
498
|
.suite-settings-sidebar {
|
|
461
|
-
padding: 0.375rem;
|
|
499
|
+
padding: 0.375rem 0.375rem 0.75rem;
|
|
462
500
|
border: 1px solid var(--ph-border-subtle);
|
|
463
501
|
border-radius: var(--ph-radius-surface);
|
|
464
502
|
background: var(--ph-surface-inset);
|
|
@@ -480,6 +518,33 @@ html[data-marketing-paused] .marketing-landing::before {
|
|
|
480
518
|
.today-page-frame .suite-page-inset {
|
|
481
519
|
padding-top: var(--suite-today-pad-y-top);
|
|
482
520
|
}
|
|
521
|
+
|
|
522
|
+
/* Start the body below the sticky header's fade, with a clear 16px gap. */
|
|
523
|
+
.today-page-frame .suite-page-header + .suite-page-body {
|
|
524
|
+
margin-top: calc(var(--suite-scroll-fade) + var(--suite-space-4));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.today-work-row {
|
|
528
|
+
display: flex;
|
|
529
|
+
min-height: 2.75rem;
|
|
530
|
+
min-width: 0;
|
|
531
|
+
align-items: flex-start;
|
|
532
|
+
gap: 0.75rem;
|
|
533
|
+
padding: 0.875rem 1rem;
|
|
534
|
+
border-radius: var(--ph-radius-control);
|
|
535
|
+
transition: background-color var(--ph-motion-fast) ease;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
:is(a, button).today-work-row:hover {
|
|
539
|
+
background: var(--ph-surface-hover);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
:is(a, button).today-work-row:focus-visible {
|
|
543
|
+
outline: none;
|
|
544
|
+
box-shadow: inset 0 0 0 3px
|
|
545
|
+
color-mix(in oklab, var(--ph-focus-ring) 30%, transparent);
|
|
546
|
+
}
|
|
547
|
+
|
|
483
548
|
@media (min-width: 640px) {
|
|
484
549
|
.today-page-frame .suite-page-inset {
|
|
485
550
|
padding-top: var(--suite-today-pad-y-top-sm);
|
|
@@ -734,40 +799,7 @@ html.dark .today-page-frame__cloud--2 {
|
|
|
734
799
|
}
|
|
735
800
|
}
|
|
736
801
|
|
|
737
|
-
/* Public landing nav
|
|
738
|
-
the page background shows through and only scrolling content dissolves. */
|
|
739
|
-
.landing-page-nav {
|
|
740
|
-
isolation: isolate;
|
|
741
|
-
background: transparent;
|
|
742
|
-
border-bottom: none;
|
|
743
|
-
}
|
|
744
|
-
.landing-page-nav::before {
|
|
745
|
-
content: "";
|
|
746
|
-
position: absolute;
|
|
747
|
-
inset-inline: 0;
|
|
748
|
-
top: 0;
|
|
749
|
-
height: calc(100% + var(--suite-scroll-fade));
|
|
750
|
-
z-index: -1;
|
|
751
|
-
background: linear-gradient(
|
|
752
|
-
to bottom,
|
|
753
|
-
color-mix(in srgb, var(--ph-canvas) 18%, transparent)
|
|
754
|
-
calc(100% - var(--suite-scroll-fade)),
|
|
755
|
-
transparent
|
|
756
|
-
);
|
|
757
|
-
-webkit-backdrop-filter: blur(8px);
|
|
758
|
-
backdrop-filter: blur(8px);
|
|
759
|
-
-webkit-mask-image: linear-gradient(
|
|
760
|
-
to bottom,
|
|
761
|
-
#000 calc(100% - var(--suite-scroll-fade)),
|
|
762
|
-
transparent
|
|
763
|
-
);
|
|
764
|
-
mask-image: linear-gradient(
|
|
765
|
-
to bottom,
|
|
766
|
-
#000 calc(100% - var(--suite-scroll-fade)),
|
|
767
|
-
transparent
|
|
768
|
-
);
|
|
769
|
-
pointer-events: none;
|
|
770
|
-
}
|
|
802
|
+
/* Public landing nav (see the shared header rule above for its fade layer). */
|
|
771
803
|
|
|
772
804
|
/* Page stage — ONE layout standard for every suite app.
|
|
773
805
|
- Default width="full": use the full main column (no max-width centring)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
const EDITABLE_FIELDS: Record<string, string[]> = {
|
|
4
|
+
propose_project_handover: ["title", "body"],
|
|
5
|
+
propose_requirement_update: ["description"],
|
|
6
|
+
propose_source_tasks: ["tasks"],
|
|
7
|
+
propose_source_task_update: ["description"],
|
|
8
|
+
propose_knowledge_pack: ["title", "body", "tasks"],
|
|
9
|
+
propose_document_update: ["body"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function suiteAiDraftFields(
|
|
13
|
+
kind: string,
|
|
14
|
+
payload?: Record<string, unknown>,
|
|
15
|
+
): Record<string, unknown> | null {
|
|
16
|
+
const fields = EDITABLE_FIELDS[kind];
|
|
17
|
+
return fields && payload
|
|
18
|
+
? Object.fromEntries(
|
|
19
|
+
fields.map((key) => [key, payload[key] ?? (key === "tasks" ? [] : "")]),
|
|
20
|
+
)
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SuiteAiDraftEditor({
|
|
25
|
+
kind,
|
|
26
|
+
draft,
|
|
27
|
+
canAddTasks,
|
|
28
|
+
onChange,
|
|
29
|
+
}: {
|
|
30
|
+
kind: string;
|
|
31
|
+
draft: Record<string, unknown>;
|
|
32
|
+
canAddTasks: boolean;
|
|
33
|
+
onChange: (draft: Record<string, unknown>) => void;
|
|
34
|
+
}) {
|
|
35
|
+
const change = (key: string, value: unknown) =>
|
|
36
|
+
onChange({ ...draft, [key]: value });
|
|
37
|
+
const tasks = Array.isArray(draft.tasks)
|
|
38
|
+
? (draft.tasks as Record<string, unknown>[])
|
|
39
|
+
: [];
|
|
40
|
+
const editTask = (index: number, key: string, value: unknown) =>
|
|
41
|
+
change(
|
|
42
|
+
"tasks",
|
|
43
|
+
tasks.map((task, i) => (i === index ? { ...task, [key]: value } : task)),
|
|
44
|
+
);
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className="mt-3 space-y-3 rounded-lg border border-ph-border bg-ph-canvas p-3"
|
|
48
|
+
data-test="ai-draft-editor"
|
|
49
|
+
>
|
|
50
|
+
<p className="text-xs text-ph-mutedtext">
|
|
51
|
+
Edit draft content. Source evidence, versions and destination remain
|
|
52
|
+
protected.
|
|
53
|
+
</p>
|
|
54
|
+
{Object.entries(draft)
|
|
55
|
+
.filter(([key]) => key !== "tasks")
|
|
56
|
+
.map(([key, value]) => (
|
|
57
|
+
<label key={key} className="block space-y-1 text-sm">
|
|
58
|
+
<span className="font-medium">
|
|
59
|
+
{key === "title" ? "Title" : "Proposed content"}
|
|
60
|
+
</span>
|
|
61
|
+
{key === "title" ? (
|
|
62
|
+
<input
|
|
63
|
+
className="ph-input min-h-11 w-full"
|
|
64
|
+
value={String(value)}
|
|
65
|
+
maxLength={255}
|
|
66
|
+
onChange={(event) => change(key, event.target.value)}
|
|
67
|
+
/>
|
|
68
|
+
) : (
|
|
69
|
+
<textarea
|
|
70
|
+
className="ph-input min-h-40 w-full"
|
|
71
|
+
value={String(value)}
|
|
72
|
+
maxLength={kind === "propose_project_handover" ? 16000 : 12000}
|
|
73
|
+
onChange={(event) => change(key, event.target.value)}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</label>
|
|
77
|
+
))}
|
|
78
|
+
{"tasks" in draft ? (
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
<p className="text-sm font-medium">Task drafts</p>
|
|
81
|
+
{tasks.map((task, index) => (
|
|
82
|
+
<fieldset
|
|
83
|
+
key={index}
|
|
84
|
+
className="space-y-2 rounded border border-ph-border p-2"
|
|
85
|
+
>
|
|
86
|
+
<legend className="px-1 text-xs">Task {index + 1}</legend>
|
|
87
|
+
<label className="block text-sm">
|
|
88
|
+
Title
|
|
89
|
+
<input
|
|
90
|
+
className="ph-input min-h-11 w-full"
|
|
91
|
+
value={String(task.title ?? "")}
|
|
92
|
+
maxLength={255}
|
|
93
|
+
onChange={(event) =>
|
|
94
|
+
editTask(index, "title", event.target.value)
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
</label>
|
|
98
|
+
<label className="block text-sm">
|
|
99
|
+
Description
|
|
100
|
+
<textarea
|
|
101
|
+
className="ph-input min-h-24 w-full"
|
|
102
|
+
value={String(task.description ?? "")}
|
|
103
|
+
maxLength={10000}
|
|
104
|
+
onChange={(event) =>
|
|
105
|
+
editTask(index, "description", event.target.value)
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
</label>
|
|
109
|
+
<div className="flex flex-wrap gap-2">
|
|
110
|
+
<label className="text-sm">
|
|
111
|
+
Due date
|
|
112
|
+
<input
|
|
113
|
+
type="date"
|
|
114
|
+
className="ph-input min-h-11 block"
|
|
115
|
+
value={String(task.due_date ?? "")}
|
|
116
|
+
onChange={(event) =>
|
|
117
|
+
editTask(index, "due_date", event.target.value || null)
|
|
118
|
+
}
|
|
119
|
+
/>
|
|
120
|
+
</label>
|
|
121
|
+
<label className="text-sm">
|
|
122
|
+
Priority
|
|
123
|
+
<select
|
|
124
|
+
className="ph-input min-h-11 block"
|
|
125
|
+
value={String(task.priority ?? "low")}
|
|
126
|
+
onChange={(event) =>
|
|
127
|
+
editTask(index, "priority", event.target.value)
|
|
128
|
+
}
|
|
129
|
+
>
|
|
130
|
+
{["low", "medium", "high", "urgent"].map((value) => (
|
|
131
|
+
<option key={value}>{value}</option>
|
|
132
|
+
))}
|
|
133
|
+
</select>
|
|
134
|
+
</label>
|
|
135
|
+
</div>
|
|
136
|
+
<label className="flex min-h-11 items-center gap-2 text-sm">
|
|
137
|
+
<input
|
|
138
|
+
type="checkbox"
|
|
139
|
+
checked={Boolean(task.assign_me)}
|
|
140
|
+
onChange={(event) =>
|
|
141
|
+
editTask(index, "assign_me", event.target.checked)
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
Assign to me
|
|
145
|
+
</label>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
className="ph-btn ph-btn-secondary ph-btn-xs"
|
|
149
|
+
disabled={kind === "propose_source_tasks" && tasks.length === 1}
|
|
150
|
+
onClick={() =>
|
|
151
|
+
change(
|
|
152
|
+
"tasks",
|
|
153
|
+
tasks.filter((_, i) => i !== index),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
Remove draft
|
|
158
|
+
</button>
|
|
159
|
+
</fieldset>
|
|
160
|
+
))}
|
|
161
|
+
{canAddTasks && tasks.length < 5 ? (
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
className="ph-btn ph-btn-secondary ph-btn-sm"
|
|
165
|
+
onClick={() =>
|
|
166
|
+
change("tasks", [
|
|
167
|
+
...tasks,
|
|
168
|
+
{
|
|
169
|
+
title: "",
|
|
170
|
+
description: "",
|
|
171
|
+
priority: "low",
|
|
172
|
+
assign_me: false,
|
|
173
|
+
},
|
|
174
|
+
])
|
|
175
|
+
}
|
|
176
|
+
>
|
|
177
|
+
Add task draft
|
|
178
|
+
</button>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cleanup,
|
|
3
|
+
fireEvent,
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
waitFor,
|
|
7
|
+
} from "@testing-library/react";
|
|
8
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { SuiteAiPanel, type SuiteAiAction } from "./ai-panel";
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
// jsdom has no layout/scroll implementation; the real panel uses this API.
|
|
13
|
+
HTMLElement.prototype.scrollTo = vi.fn();
|
|
14
|
+
});
|
|
15
|
+
afterEach(cleanup);
|
|
16
|
+
|
|
17
|
+
function TestSpinner() {
|
|
18
|
+
return <span aria-hidden="true" />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("integration draft review", () => {
|
|
22
|
+
it("shows the handover and source revisions without applying it", async () => {
|
|
23
|
+
const action: SuiteAiAction = {
|
|
24
|
+
id: "handover",
|
|
25
|
+
kind: "propose_project_handover",
|
|
26
|
+
status: "proposed",
|
|
27
|
+
summary: "Save a private project handover",
|
|
28
|
+
payload: {
|
|
29
|
+
body: "Open work and next steps from the brief.",
|
|
30
|
+
sources: [{ title: "Project brief", revision_number: 3 }],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const resolveAction = vi.fn();
|
|
34
|
+
const fetchChat = vi.fn().mockResolvedValue({
|
|
35
|
+
configured: true,
|
|
36
|
+
messages: [
|
|
37
|
+
{ role: "assistant", content: "Review this draft.", actions: [action] },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
render(
|
|
41
|
+
<SuiteAiPanel
|
|
42
|
+
spinner={TestSpinner}
|
|
43
|
+
clearChat={vi.fn().mockResolvedValue(undefined)}
|
|
44
|
+
open
|
|
45
|
+
fetchChat={fetchChat}
|
|
46
|
+
sendMessage={vi.fn()}
|
|
47
|
+
resolveAction={resolveAction}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
fireEvent.click(
|
|
51
|
+
await screen.findByText("Review proposed content and evidence"),
|
|
52
|
+
);
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByText("Open work and next steps from the brief."),
|
|
55
|
+
).toBeVisible();
|
|
56
|
+
expect(screen.getByText(/"revision_number": 3/)).toBeVisible();
|
|
57
|
+
expect(resolveAction).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("shows before/after content and delegates only explicit confirmation", async () => {
|
|
61
|
+
const action: SuiteAiAction = {
|
|
62
|
+
id: "requirements",
|
|
63
|
+
kind: "propose_requirement_update",
|
|
64
|
+
status: "proposed",
|
|
65
|
+
summary: "Update acceptance criteria",
|
|
66
|
+
payload: {
|
|
67
|
+
before: "Existing human note.",
|
|
68
|
+
description: "Existing human note. Added acceptance criterion.",
|
|
69
|
+
document_revision: 2,
|
|
70
|
+
task_updated_at: "2026-10-06T10:00:00Z",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const messages = [
|
|
74
|
+
{
|
|
75
|
+
role: "assistant" as const,
|
|
76
|
+
content: "Review the change.",
|
|
77
|
+
actions: [action],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const resolveAction = vi.fn().mockResolvedValue({ messages });
|
|
81
|
+
render(
|
|
82
|
+
<SuiteAiPanel
|
|
83
|
+
spinner={TestSpinner}
|
|
84
|
+
clearChat={vi.fn().mockResolvedValue(undefined)}
|
|
85
|
+
open
|
|
86
|
+
fetchChat={vi.fn().mockResolvedValue({ configured: true, messages })}
|
|
87
|
+
sendMessage={vi.fn()}
|
|
88
|
+
resolveAction={resolveAction}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
fireEvent.click(
|
|
92
|
+
await screen.findByText("Review proposed content and evidence"),
|
|
93
|
+
);
|
|
94
|
+
expect(screen.getByText("Existing human note.")).toBeVisible();
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByText("Existing human note. Added acceptance criterion."),
|
|
97
|
+
).toBeVisible();
|
|
98
|
+
expect(resolveAction).not.toHaveBeenCalled();
|
|
99
|
+
fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
|
|
100
|
+
await waitFor(() =>
|
|
101
|
+
expect(resolveAction).toHaveBeenCalledWith("requirements", "confirm"),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("keeps workflows available in an existing chat and only prefills the composer", async () => {
|
|
106
|
+
const sendMessage = vi.fn();
|
|
107
|
+
render(
|
|
108
|
+
<SuiteAiPanel
|
|
109
|
+
spinner={TestSpinner}
|
|
110
|
+
clearChat={vi.fn()}
|
|
111
|
+
open
|
|
112
|
+
fetchChat={vi
|
|
113
|
+
.fn()
|
|
114
|
+
.mockResolvedValue({
|
|
115
|
+
configured: true,
|
|
116
|
+
messages: [{ role: "assistant", content: "Existing conversation" }],
|
|
117
|
+
})}
|
|
118
|
+
sendMessage={sendMessage}
|
|
119
|
+
presets={[
|
|
120
|
+
{
|
|
121
|
+
label: "Prepare knowledge pack",
|
|
122
|
+
prompt: "Prepare a reviewed knowledge pack.",
|
|
123
|
+
},
|
|
124
|
+
]}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
await screen.findByRole("button", { name: "Clear" });
|
|
128
|
+
fireEvent.click(screen.getByText("Workflows"));
|
|
129
|
+
fireEvent.click(
|
|
130
|
+
screen.getByRole("button", { name: "Prepare knowledge pack" }),
|
|
131
|
+
);
|
|
132
|
+
expect(
|
|
133
|
+
screen.getByDisplayValue("Prepare a reviewed knowledge pack."),
|
|
134
|
+
).toBeVisible();
|
|
135
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("submits human-edited task drafts without exposing scope as an editable field", async () => {
|
|
139
|
+
const action: SuiteAiAction = {
|
|
140
|
+
id: "tasks",
|
|
141
|
+
kind: "propose_source_tasks",
|
|
142
|
+
status: "proposed",
|
|
143
|
+
summary: "Create source tasks",
|
|
144
|
+
payload: {
|
|
145
|
+
project_id: "project-1",
|
|
146
|
+
tasks: [{ title: "Initial task", description: "Criteria" }],
|
|
147
|
+
requested_by: "user-1",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const messages = [
|
|
151
|
+
{
|
|
152
|
+
role: "assistant" as const,
|
|
153
|
+
content: "Review task drafts.",
|
|
154
|
+
actions: [action],
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
const resolveAction = vi.fn().mockResolvedValue({ messages });
|
|
158
|
+
render(
|
|
159
|
+
<SuiteAiPanel
|
|
160
|
+
spinner={TestSpinner}
|
|
161
|
+
clearChat={vi.fn()}
|
|
162
|
+
open
|
|
163
|
+
fetchChat={vi.fn().mockResolvedValue({ configured: true, messages })}
|
|
164
|
+
sendMessage={vi.fn()}
|
|
165
|
+
resolveAction={resolveAction}
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
fireEvent.click(await screen.findByRole("button", { name: "Edit draft" }));
|
|
169
|
+
fireEvent.change(screen.getByLabelText("Title"), {
|
|
170
|
+
target: { value: "Human-edited task" },
|
|
171
|
+
});
|
|
172
|
+
fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
|
|
173
|
+
await waitFor(() =>
|
|
174
|
+
expect(resolveAction).toHaveBeenCalledWith("tasks", "confirm", {
|
|
175
|
+
tasks: [{ title: "Human-edited task", description: "Criteria" }],
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
});
|