@xenide-io/the-old-ui-theme 0.9.18 → 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-BzuNgJIa.d.mts → Chip-Dw2g2t8D.d.mts} +18 -4
- package/dist/{Chip-BzuNgJIa.d.ts → Chip-Dw2g2t8D.d.ts} +18 -4
- package/dist/{chunk-6SVJAU5K.mjs → chunk-RIPGN6FG.mjs} +1333 -526
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +490 -119
- 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 +36 -4
- package/dist/suite.d.ts +36 -4
- package/dist/suite.js +4617 -4001
- package/dist/suite.mjs +1456 -868
- package/dist/tailwind-preset.js +10 -1
- package/dist/tailwind-preset.mjs +10 -1
- package/dist/ui.d.mts +33 -4
- package/dist/ui.d.ts +33 -4
- package/dist/ui.js +1364 -571
- package/dist/ui.mjs +15 -1
- package/docs/macos-identity-audit.md +279 -0
- package/package.json +1 -1
- package/src/app/globals.css +390 -44
- package/src/components/ui/Accordion.tsx +4 -7
- package/src/components/ui/Avatar.tsx +5 -5
- package/src/components/ui/BrandSpinner.tsx +256 -0
- package/src/components/ui/Calendar.tsx +7 -7
- package/src/components/ui/CodeBlock.tsx +14 -11
- package/src/components/ui/CollapsibleSection.tsx +4 -4
- package/src/components/ui/CommandPalette.tsx +7 -10
- package/src/components/ui/EmptyState.tsx +2 -2
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Marketing.tsx +144 -0
- package/src/components/ui/MarketingIllustrations.tsx +287 -0
- package/src/components/ui/Panel.tsx +1 -1
- package/src/components/ui/SegmentedControl.tsx +59 -13
- package/src/components/ui/SettingsLayout.tsx +4 -0
- package/src/components/ui/ShowcaseWrapper.tsx +3 -3
- package/src/components/ui/Table.tsx +45 -10
- package/src/components/ui/Typography.tsx +4 -4
- package/src/components/ui/avatar-stat.test.tsx +14 -1
- package/src/components/ui/brand-spinner.test.tsx +62 -0
- package/src/components/ui/index.ts +19 -11
- package/src/components/ui/interaction-primitives.test.tsx +12 -6
- package/src/components/ui/marketing.test.tsx +39 -0
- package/src/components/ui/table.test.tsx +47 -0
- package/src/styles/suite-skin.css +471 -74
- package/src/styles/themes.css +45 -45
- package/src/styles/typography.css +25 -1
- 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 +201 -31
- package/src/suite/components/app-switcher.tsx +22 -9
- package/src/suite/components/command-palette-host.tsx +20 -2
- package/src/suite/components/suite-app-layout.tsx +7 -4
- package/src/suite/components/suite-bottom-nav.tsx +1 -1
- package/src/suite/components/suite-clients-projects-directory.tsx +6 -6
- package/src/suite/components/suite-integration-picker.tsx +7 -7
- package/src/suite/components/suite-integration-rules.tsx +5 -5
- package/src/suite/components/suite-layout.test.tsx +32 -3
- package/src/suite/components/suite-layout.tsx +4 -4
- package/src/suite/components/suite-mobile-drawer.tsx +2 -2
- package/src/suite/components/suite-mobile-header.tsx +1 -1
- package/src/suite/components/suite-notification-bell.tsx +27 -6
- package/src/suite/components/suite-notification-settings.tsx +182 -0
- package/src/suite/components/suite-palette-library.test.tsx +46 -0
- package/src/suite/components/suite-palette-library.tsx +82 -15
- package/src/suite/components/suite-settings-layout.tsx +2 -2
- package/src/suite/components/suite-settings-mobile-nav.tsx +10 -10
- package/src/suite/components/suite-sidebar.test.tsx +39 -0
- package/src/suite/components/suite-sidebar.tsx +41 -19
- package/src/suite/components/suite-skeleton.tsx +6 -6
- package/src/suite/components/suite-user-menu.tsx +2 -2
- package/src/suite/components/theme-provider.tsx +2 -0
- package/src/suite/components/today-calibrating.tsx +3 -3
- package/src/suite/components/today-ui.tsx +42 -37
- package/src/suite/components/workspace-switcher.tsx +3 -3
- package/src/suite/index.ts +14 -0
- package/src/suite/lib/suite-notifications.tsx +5 -0
- package/tailwind-preset.ts +9 -0
package/src/styles/themes.css
CHANGED
|
@@ -87,55 +87,55 @@
|
|
|
87
87
|
[data-theme="hedgehog-dark"] {
|
|
88
88
|
color-scheme: dark;
|
|
89
89
|
|
|
90
|
-
/*
|
|
91
|
-
--ph-canvas: hsl(
|
|
92
|
-
--ph-surface: hsl(
|
|
93
|
-
--ph-muted: hsl(
|
|
94
|
-
--ph-toolbar: hsl(
|
|
95
|
-
--ph-surface-inset: hsl(
|
|
96
|
-
--ph-surface-raised: hsl(
|
|
97
|
-
--ph-surface-overlay: hsl(
|
|
98
|
-
--ph-border: hsl(
|
|
99
|
-
--ph-border-subtle: hsl(
|
|
100
|
-
--ph-border-strong: hsl(
|
|
101
|
-
|
|
102
|
-
--ph-text-primary: hsl(
|
|
103
|
-
--ph-text-secondary: hsl(
|
|
104
|
-
--ph-text-tertiary: hsl(
|
|
105
|
-
|
|
106
|
-
--ph-accent: #
|
|
107
|
-
--ph-accent-hover: #
|
|
108
|
-
--ph-blue: #
|
|
109
|
-
--ph-violet: #
|
|
110
|
-
--ph-purple: #
|
|
111
|
-
|
|
112
|
-
--ph-success: #
|
|
113
|
-
--ph-warning: #
|
|
114
|
-
--ph-danger: #
|
|
115
|
-
--ph-info: #
|
|
116
|
-
--ph-on-accent: #
|
|
90
|
+
/* Warm graphite keeps the Hedgehog pair recognisable after dark. */
|
|
91
|
+
--ph-canvas: hsl(36 8% 7%);
|
|
92
|
+
--ph-surface: hsl(34 7% 10%);
|
|
93
|
+
--ph-muted: hsl(32 7% 13%);
|
|
94
|
+
--ph-toolbar: hsl(30 7% 16%);
|
|
95
|
+
--ph-surface-inset: hsl(36 8% 6%);
|
|
96
|
+
--ph-surface-raised: hsl(32 7% 14%);
|
|
97
|
+
--ph-surface-overlay: hsl(30 7% 16%);
|
|
98
|
+
--ph-border: hsl(31 6% 22%);
|
|
99
|
+
--ph-border-subtle: hsl(32 6% 17%);
|
|
100
|
+
--ph-border-strong: hsl(30 5% 38%);
|
|
101
|
+
|
|
102
|
+
--ph-text-primary: hsl(40 18% 94%);
|
|
103
|
+
--ph-text-secondary: hsl(36 10% 76%);
|
|
104
|
+
--ph-text-tertiary: hsl(34 7% 59%);
|
|
105
|
+
|
|
106
|
+
--ph-accent: #e8682a;
|
|
107
|
+
--ph-accent-hover: #f27b3d;
|
|
108
|
+
--ph-blue: #5b9cff;
|
|
109
|
+
--ph-violet: #b66ce0;
|
|
110
|
+
--ph-purple: #b66ce0;
|
|
111
|
+
|
|
112
|
+
--ph-success: #63b77b;
|
|
113
|
+
--ph-warning: #e6ad48;
|
|
114
|
+
--ph-danger: #df625c;
|
|
115
|
+
--ph-info: #5b9cff;
|
|
116
|
+
--ph-on-accent: #1a0e08;
|
|
117
117
|
--ph-on-violet: #ffffff;
|
|
118
118
|
--ph-on-success: #111114;
|
|
119
119
|
--ph-on-warning: #111114;
|
|
120
120
|
--ph-on-danger: #ffffff;
|
|
121
|
-
--ph-on-info: #
|
|
122
|
-
--ph-focus-ring: #
|
|
123
|
-
--ph-selected-bg:
|
|
124
|
-
--ph-selected-text:
|
|
125
|
-
--ph-selected-border:
|
|
126
|
-
|
|
127
|
-
--ph-text-3000:
|
|
128
|
-
--ph-primary-button-face: #
|
|
129
|
-
--ph-primary-button-border: #
|
|
130
|
-
--ph-primary-button-border-hover: #
|
|
131
|
-
--ph-primary-frame-bg: #
|
|
132
|
-
--ph-secondary-button-bg: hsl(
|
|
133
|
-
--ph-secondary-frame-bg: hsl(
|
|
134
|
-
--ph-secondary-button-border: hsl(
|
|
135
|
-
--ph-secondary-button-border-hover: hsl(
|
|
136
|
-
|
|
137
|
-
--ph-border-3000: hsl(
|
|
138
|
-
--ph-accent-highlight-secondary:
|
|
121
|
+
--ph-on-info: #101725;
|
|
122
|
+
--ph-focus-ring: #66a5ff;
|
|
123
|
+
--ph-selected-bg: hsl(19 72% 17%);
|
|
124
|
+
--ph-selected-text: hsl(22 92% 76%);
|
|
125
|
+
--ph-selected-border: hsl(19 66% 48%);
|
|
126
|
+
|
|
127
|
+
--ph-text-3000: #1a0e08;
|
|
128
|
+
--ph-primary-button-face: #ef8a4c;
|
|
129
|
+
--ph-primary-button-border: #c75a24;
|
|
130
|
+
--ph-primary-button-border-hover: #e8682a;
|
|
131
|
+
--ph-primary-frame-bg: #883714;
|
|
132
|
+
--ph-secondary-button-bg: hsl(30 7% 16%);
|
|
133
|
+
--ph-secondary-frame-bg: hsl(30 7% 20%);
|
|
134
|
+
--ph-secondary-button-border: hsl(30 5% 31%);
|
|
135
|
+
--ph-secondary-button-border-hover: hsl(30 5% 44%);
|
|
136
|
+
|
|
137
|
+
--ph-border-3000: hsl(30 7% 18%);
|
|
138
|
+
--ph-accent-highlight-secondary: hsl(19 82% 56% / 16%);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/* OneNote — brand purple #7719aa on lavender paper. */
|
|
@@ -6,10 +6,25 @@
|
|
|
6
6
|
* 2. Monospace stack — code, CSV, token chips, shortcuts. Functional only.
|
|
7
7
|
*
|
|
8
8
|
* Marketing type uses `clamp()` so a hero has ONE definition instead of three
|
|
9
|
-
* breakpoint variants per app. Product
|
|
9
|
+
* breakpoint variants per app. Product roles and Tailwind text utilities share
|
|
10
|
+
* the tokens below, including every suite component and consuming app.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
:root {
|
|
14
|
+
--ph-font-sans-stack: var(--font-open-runde, "Open Runde"), ui-sans-serif, system-ui, sans-serif;
|
|
15
|
+
/* Product scale: comfortable body/control text, compact readable metadata. */
|
|
16
|
+
--ph-type-page: 1.5rem;
|
|
17
|
+
--ph-type-product-section: clamp(1.375rem, 1.25rem + 0.5vw, 1.5rem);
|
|
18
|
+
--ph-type-subsection: 1.25rem;
|
|
19
|
+
--ph-type-card: 1.125rem;
|
|
20
|
+
--ph-type-body: 1rem;
|
|
21
|
+
--ph-type-ui: 0.9375rem;
|
|
22
|
+
--ph-type-small: 0.9375rem;
|
|
23
|
+
--ph-type-meta: 0.8125rem;
|
|
24
|
+
--ph-type-overline: 0.75rem;
|
|
25
|
+
--ph-leading-body: 1.6;
|
|
26
|
+
--ph-leading-ui: 1.5;
|
|
27
|
+
--ph-leading-title: 1.2;
|
|
13
28
|
/* Code / CSV / token chips — monospace with clear ASCII */
|
|
14
29
|
--ph-font-monospace-stack:
|
|
15
30
|
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
|
@@ -30,6 +45,15 @@
|
|
|
30
45
|
--ph-type-lead: clamp(1.0625rem, 1rem + 0.35vw, 1.25rem);
|
|
31
46
|
}
|
|
32
47
|
|
|
48
|
+
@media (max-width: 639px) {
|
|
49
|
+
:root {
|
|
50
|
+
--ph-type-page: 1.375rem;
|
|
51
|
+
--ph-type-ui: 1rem;
|
|
52
|
+
--ph-type-small: 1rem;
|
|
53
|
+
--ph-type-meta: 0.875rem;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/* ── Marketing / landing type ─────────────────────────────────────────────
|
|
34
58
|
Use these on public pages so all five apps share one hero rhythm.
|
|
35
59
|
In-app chrome keeps the Tailwind step scale; do not use hero classes
|
|
@@ -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
|
+
});
|