@jskit-ai/assistant-core 0.2.24 → 0.2.25
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/CHANGELOG.md +4 -1
- package/examples/conversation/package.json +6 -6
- package/package.json +8 -8
- package/src/client/components/AssistantSettingsFormCard.vue +14 -9
- package/src/client/conversation/AssistantAttachmentQueue.vue +2 -4
- package/src/client/conversation/AssistantConversationElement.vue +2 -2
- package/src/client/conversation/AssistantMessageAttachments.vue +2 -2
- package/src/client/conversation/AssistantModelControl.vue +5 -4
- package/src/client/conversation/AssistantPromptInput.vue +24 -18
- package/src/client/conversation/AssistantTranscript.css +7 -15
- package/src/client/conversation/LongTextInlineParts.vue +2 -3
- package/src/client/conversation/LongTextPreviewBlocks.vue +7 -6
- package/src/server/conversation/codexProviderOwner.js +6 -1
- package/test/assistantScroll.browser.test.js +53 -103
- package/test/claudeConversationSession.test.js +3 -1
- package/test/componentContracts.test.js +44 -0
- package/test/conversationAvatarThumbnail.test.js +3 -4
- package/test/messageDelivery.browser.test.js +12 -14
- package/test/opencodeConversationSession.test.js +20 -4
- package/test/transcriptTheme.browser.test.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
- The
|
|
5
|
+
- The conversation's remaining boxes are Material 3 tonal containers instead of outlines, visual only: queued and sent attachments and the questions are `surface-container-high`, code blocks and inline code `surface-container-highest`, tables and details `surface-container-low` (a table keeps `outline-variant` row dividers), question numbers `secondary-container` badges, and the model menu a `surface-container` surface at elevation level 2 with `outline-variant` dividers. The composer stays Material 3's outlined text field, the only outline left.
|
|
6
|
+
- `AssistantSettingsFormCard` takes a `formId`: its form gets that id and the card draws no Save button of its own, so the page's app bar can carry Save as a submit button for the form (`type="submit" form="<formId>"`), as a Material 3 settings pane does. Without one, the card keeps its Save; a person who may not save still sees the read-only note.
|
|
7
|
+
- Material 3 containers in the conversation, visual only: the transcript area is no longer outlined (its bubbles are the only containers, and the welcome bubble is a tonal fill without an outline), the composer is Material 3's outlined text field (4dp corners, the `outline` role, a 2dp primary outline on focus, 16dp side padding) with Send and the other actions 12dp from it (8dp when compact), and `AssistantSettingsFormCard` adds no heading or divider inside the settings pane that titles it: its subtitle is the card's first line and its title names the section for assistive technology.
|
|
8
|
+
- The avatar's Show and Minimise buttons set no fixed size: they are 40dp icon buttons with material-web's 48dp touch target, so `jskit check m3` passes on `AssistantConversationElement`. The collapsed Show button is 40dp wide like Minimise, no longer 44dp.
|
|
6
9
|
- The goal menu closes when Cancel succeeds, as it does after Start; Pause and Resume keep it open.
|
|
7
10
|
- `AssistantConversationElement` has an `avatar-thumbnail` slot, with the `{ size, height }` scope of the other avatar slots: while the avatar is hidden, the **Show avatar** button draws its content instead of the anonymous account icon, such as a still miniature of the avatar, clipped to the button's round 32-pixel disc. The button keeps its label, title and 44-pixel target, and shows the icon as before without the slot.
|
|
8
11
|
- A delivered Codex turn is held as not yet started only until Codex reports it: a `turn/started` that arrives before the `turn/start` answer counts, and a thread read listing exactly that turn as completed or failed settles a turn whose notifications were missed, so its answer is no longer kept waiting for a notification that never comes.
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.2.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
13
|
-
"@jskit-ai/kernel": "0.2.
|
|
14
|
-
"@jskit-ai/material-web": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.2.25",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.2.11",
|
|
13
|
+
"@jskit-ai/kernel": "0.2.11",
|
|
14
|
+
"@jskit-ai/material-web": "0.1.3",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.2.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.2.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.2.12",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.2.12"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "
|
|
6
|
+
"test": "jskit test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
52
52
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
53
53
|
"@ai-sdk/xai": "^4.0.58",
|
|
54
|
-
"@jskit-ai/agent": "0.1.
|
|
55
|
-
"@jskit-ai/resource-core": "0.1.
|
|
56
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
54
|
+
"@jskit-ai/agent": "0.1.2",
|
|
55
|
+
"@jskit-ai/resource-core": "0.1.202",
|
|
56
|
+
"@jskit-ai/resource-crud-core": "0.1.202",
|
|
57
57
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
58
58
|
"ai": "^7.0.152",
|
|
59
59
|
"json-rest-schema": "^1.0.17",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"ws": "^8.20.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
65
|
-
"@jskit-ai/kernel": "0.2.
|
|
64
|
+
"@jskit-ai/http-runtime": "0.2.11",
|
|
65
|
+
"@jskit-ai/kernel": "0.2.11",
|
|
66
66
|
"vue": "^3.5.13",
|
|
67
67
|
"vuetify": "^4.3.0"
|
|
68
68
|
},
|
|
@@ -220,6 +220,6 @@
|
|
|
220
220
|
}
|
|
221
221
|
},
|
|
222
222
|
"devDependencies": {
|
|
223
|
-
"@jskit-ai/material-web": "0.1.
|
|
223
|
+
"@jskit-ai/material-web": "0.1.3"
|
|
224
224
|
}
|
|
225
225
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- The settings pane around it carries the page title, so the card adds no heading of its own: the title names the
|
|
3
|
+
section for assistive technology and the subtitle says what the settings apply to. -->
|
|
4
|
+
<section :class="props.rootClass" :aria-label="props.title">
|
|
3
5
|
<v-card>
|
|
4
|
-
<v-card-
|
|
5
|
-
<
|
|
6
|
-
<v-card-subtitle>{{ props.subtitle }}</v-card-subtitle>
|
|
7
|
-
</v-card-item>
|
|
8
|
-
<v-divider />
|
|
9
|
-
<v-card-text class="pt-4">
|
|
6
|
+
<v-card-text>
|
|
7
|
+
<p class="text-body-medium on-surface-variant mb-4">{{ props.subtitle }}</p>
|
|
10
8
|
<template v-if="props.showFormSkeleton">
|
|
11
9
|
<v-skeleton-loader type="text@2, list-item-two-line@4, button" />
|
|
12
10
|
</template>
|
|
@@ -20,10 +18,12 @@
|
|
|
20
18
|
</p>
|
|
21
19
|
|
|
22
20
|
<template v-else>
|
|
23
|
-
|
|
21
|
+
<!-- With a formId, the page's app bar carries Save (a submit button for this form), as a settings pane does;
|
|
22
|
+
the card then shows only the read-only note. -->
|
|
23
|
+
<v-form :id="props.formId || undefined" @submit.prevent="props.addEdit.submit" novalidate>
|
|
24
24
|
<v-progress-linear v-if="props.addEdit.isRefetching" indeterminate class="mb-4" />
|
|
25
25
|
<slot />
|
|
26
|
-
<div class="d-flex align-center justify-end ga-3 mt-2">
|
|
26
|
+
<div v-if="!props.formId || !props.addEdit.canSave" class="d-flex align-center justify-end ga-3 mt-2">
|
|
27
27
|
<v-btn
|
|
28
28
|
v-if="props.addEdit.canSave"
|
|
29
29
|
type="submit"
|
|
@@ -74,6 +74,11 @@ const props = defineProps({
|
|
|
74
74
|
showFormSkeleton: {
|
|
75
75
|
type: Boolean,
|
|
76
76
|
default: false
|
|
77
|
+
},
|
|
78
|
+
// The form's id, when the page's app bar carries its Save button (type="submit" form="<formId>").
|
|
79
|
+
formId: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: ""
|
|
77
82
|
}
|
|
78
83
|
});
|
|
79
84
|
</script>
|
|
@@ -307,10 +307,9 @@ onBeforeUnmount(() => {
|
|
|
307
307
|
</script>
|
|
308
308
|
|
|
309
309
|
<style scoped>
|
|
310
|
+
/* A Material 3 tonal container above the composer: a surface-container-high fill, no outline. */
|
|
310
311
|
.assistant-attachment-queue {
|
|
311
|
-
background:
|
|
312
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
313
|
-
border-bottom: 0;
|
|
312
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
314
313
|
border-radius: 14px 14px 0 0;
|
|
315
314
|
color: rgb(var(--v-theme-on-surface));
|
|
316
315
|
display: grid;
|
|
@@ -332,7 +331,6 @@ onBeforeUnmount(() => {
|
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
.assistant-attachment-queue--standalone {
|
|
335
|
-
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
336
334
|
border-radius: 10px;
|
|
337
335
|
}
|
|
338
336
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<slot name="avatar-control" :size="avatarSize" :height="avatarHeight" />
|
|
9
9
|
<v-btn
|
|
10
10
|
icon="$accountCircle" size="small"
|
|
11
|
-
variant="text"
|
|
11
|
+
variant="text"
|
|
12
12
|
class="assistant-conversation__avatar-size"
|
|
13
13
|
:aria-label="t('jskit.assistant.chat.showAvatar')" :title="t('jskit.assistant.chat.showAvatar')"
|
|
14
14
|
:aria-expanded="avatarSize !== 'hidden'" @click="toggleAvatar"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<slot name="avatar-tools" :size="avatarSize" :height="avatarHeight" />
|
|
27
27
|
<v-btn
|
|
28
28
|
icon="$minus" size="small" variant="text" color="primary"
|
|
29
|
-
|
|
29
|
+
class="assistant-conversation__avatar-size"
|
|
30
30
|
:aria-label="t('jskit.assistant.chat.minimiseAvatar')" :title="t('jskit.assistant.chat.minimiseAvatar')" :aria-expanded="true"
|
|
31
31
|
@click="toggleAvatar"
|
|
32
32
|
>
|
|
@@ -70,10 +70,10 @@ function attachmentKey(attachment, index) {
|
|
|
70
70
|
width: min(19rem, 100%);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* Each attachment is a Material 3 tonal container: a surface-container-high fill, no outline. */
|
|
73
74
|
.assistant-conversation-attachments__item {
|
|
74
75
|
align-items: center;
|
|
75
|
-
background:
|
|
76
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
76
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
77
77
|
border-radius: 10px;
|
|
78
78
|
color: rgb(var(--v-theme-on-surface));
|
|
79
79
|
min-height: 2.8rem;
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
/>
|
|
21
21
|
</template>
|
|
22
22
|
|
|
23
|
+
<!-- A Material 3 menu surface: surface-container at elevation level 2, no outline. -->
|
|
23
24
|
<v-sheet
|
|
24
25
|
:aria-label="t('jskit.assistant.model.selector')"
|
|
25
|
-
border
|
|
26
26
|
class="assistant-model-control"
|
|
27
|
-
|
|
27
|
+
color="surface-container"
|
|
28
|
+
:elevation="2"
|
|
28
29
|
rounded="xl"
|
|
29
30
|
>
|
|
30
31
|
<header class="assistant-model-control__header">
|
|
@@ -235,7 +236,7 @@ function selectVariant(value) { if (!props.changesDisabled && !props.saving) emi
|
|
|
235
236
|
|
|
236
237
|
.assistant-model-control__header {
|
|
237
238
|
align-items: center;
|
|
238
|
-
border-bottom: 1px solid
|
|
239
|
+
border-bottom: 1px solid rgb(var(--v-theme-outline-variant));
|
|
239
240
|
display: flex;
|
|
240
241
|
gap: 0.65rem;
|
|
241
242
|
padding: 0.1rem 0.1rem 0.7rem;
|
|
@@ -311,7 +312,7 @@ function selectVariant(value) { if (!props.changesDisabled && !props.saving) emi
|
|
|
311
312
|
|
|
312
313
|
.assistant-model-control__actions {
|
|
313
314
|
align-items: center;
|
|
314
|
-
border-top: 1px solid
|
|
315
|
+
border-top: 1px solid rgb(var(--v-theme-outline-variant));
|
|
315
316
|
display: grid;
|
|
316
317
|
gap: 0.4rem;
|
|
317
318
|
grid-template-columns: auto 1fr auto;
|
|
@@ -261,12 +261,13 @@ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightFo
|
|
|
261
261
|
width: 100%;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/* Material 3's outlined text field: 4dp corners, a 1dp outline that becomes a 2dp primary one on focus (drawn inset, so
|
|
265
|
+
nothing moves), 16dp side padding, and the label in the outline. */
|
|
264
266
|
.assistant-prompt-input__field {
|
|
265
267
|
background: rgb(var(--v-theme-surface));
|
|
266
|
-
border: 1px solid
|
|
267
|
-
border-radius:
|
|
268
|
+
border: 1px solid rgb(var(--v-theme-outline));
|
|
269
|
+
border-radius: 4px;
|
|
268
270
|
box-sizing: border-box;
|
|
269
|
-
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.08);
|
|
270
271
|
display: grid;
|
|
271
272
|
max-width: 100%;
|
|
272
273
|
min-width: 0;
|
|
@@ -274,29 +275,33 @@ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightFo
|
|
|
274
275
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
275
276
|
}
|
|
276
277
|
|
|
278
|
+
.assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):hover {
|
|
279
|
+
border-color: rgb(var(--v-theme-on-surface));
|
|
280
|
+
}
|
|
281
|
+
|
|
277
282
|
.assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):focus-within {
|
|
278
283
|
border-color: rgb(var(--v-theme-primary));
|
|
279
|
-
box-shadow:
|
|
280
|
-
0 0 0 2px rgba(var(--v-theme-primary), 0.28),
|
|
281
|
-
inset 0 0 0 1px rgba(var(--v-theme-primary), 0.2);
|
|
284
|
+
box-shadow: inset 0 0 0 1px rgb(var(--v-theme-primary));
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
.assistant-prompt-input__field--disabled {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
border-color: rgba(var(--v-theme-on-surface), 0.12);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):focus-within .assistant-prompt-input__label {
|
|
292
|
+
color: rgb(var(--v-theme-primary));
|
|
288
293
|
}
|
|
289
294
|
|
|
290
295
|
.assistant-prompt-input__label {
|
|
291
296
|
align-self: start;
|
|
292
297
|
background: rgb(var(--v-theme-surface));
|
|
293
|
-
color:
|
|
294
|
-
font-size: 0.
|
|
295
|
-
line-height:
|
|
296
|
-
margin: -0.5rem 0 0
|
|
297
|
-
max-width: calc(100% -
|
|
298
|
+
color: rgb(var(--v-theme-on-surface-variant));
|
|
299
|
+
font-size: 0.75rem;
|
|
300
|
+
line-height: 1rem;
|
|
301
|
+
margin: -0.5rem 0 0 12px;
|
|
302
|
+
max-width: calc(100% - 24px);
|
|
298
303
|
overflow: hidden;
|
|
299
|
-
padding-inline:
|
|
304
|
+
padding-inline: 4px;
|
|
300
305
|
text-overflow: ellipsis;
|
|
301
306
|
white-space: nowrap;
|
|
302
307
|
width: fit-content;
|
|
@@ -349,19 +354,20 @@ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightFo
|
|
|
349
354
|
opacity: 1;
|
|
350
355
|
}
|
|
351
356
|
|
|
357
|
+
/* Send and the other actions keep 12dp from the outline (8dp in the compact composer). */
|
|
352
358
|
.assistant-prompt-input__footer {
|
|
353
359
|
min-width: 0;
|
|
354
|
-
padding: 0
|
|
360
|
+
padding: 0 12px 12px;
|
|
355
361
|
}
|
|
356
362
|
|
|
357
363
|
.assistant-prompt-input--compact .assistant-prompt-input__input {
|
|
358
364
|
max-height: min(8rem, 20dvh);
|
|
359
365
|
min-height: 2.5rem;
|
|
360
|
-
padding: 0.4rem
|
|
366
|
+
padding: 0.4rem 12px 0.15rem;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
.assistant-prompt-input--compact .assistant-prompt-input__footer {
|
|
364
|
-
padding: 0
|
|
370
|
+
padding: 0 8px 8px;
|
|
365
371
|
}
|
|
366
372
|
|
|
367
373
|
.assistant-prompt-input__details {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
.assistant-transcript__plain-user { white-space: pre-wrap; margin: 0; }
|
|
2
|
+
/* The transcript is the page's own area, not a container: its bubbles are the only containers in it. */
|
|
2
3
|
.assistant-transcript {
|
|
3
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.24);
|
|
4
|
-
border-radius: 8px;
|
|
5
4
|
display: grid;
|
|
6
5
|
gap: 0.35rem;
|
|
7
6
|
grid-template-rows: minmax(0, 1fr);
|
|
@@ -87,7 +86,6 @@
|
|
|
87
86
|
|
|
88
87
|
.assistant-transcript__welcome {
|
|
89
88
|
background: rgba(var(--v-theme-primary), 0.08);
|
|
90
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.18);
|
|
91
89
|
border-radius: 16px;
|
|
92
90
|
color: rgb(var(--v-theme-on-surface));
|
|
93
91
|
display: grid;
|
|
@@ -239,10 +237,6 @@
|
|
|
239
237
|
padding: 0;
|
|
240
238
|
}
|
|
241
239
|
|
|
242
|
-
.assistant-transcript--task {
|
|
243
|
-
border-color: rgba(126, 87, 194, 0.32);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
240
|
.assistant-transcript--task .assistant-transcript__avatar--assistant {
|
|
247
241
|
background: #7e57c2;
|
|
248
242
|
}
|
|
@@ -416,10 +410,9 @@
|
|
|
416
410
|
.assistant-transcript__message--user :deep(.studio-long-text-review__table th > code),
|
|
417
411
|
.assistant-transcript__message--user :deep(.studio-long-text-review__table td > code),
|
|
418
412
|
.assistant-transcript__message--user :deep(.studio-long-text-review__details-summary code) {
|
|
419
|
-
background:
|
|
420
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.14);
|
|
413
|
+
background: rgb(var(--v-theme-surface-container-highest));
|
|
421
414
|
border-radius: 3px;
|
|
422
|
-
color:
|
|
415
|
+
color: rgb(var(--v-theme-on-surface));
|
|
423
416
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
424
417
|
font-size: 0.9em;
|
|
425
418
|
padding: 0.02rem 0.2rem;
|
|
@@ -436,10 +429,10 @@
|
|
|
436
429
|
padding: 0;
|
|
437
430
|
}
|
|
438
431
|
|
|
432
|
+
/* Questions are tonal containers (surface-container-high), their numbers secondary-container badges: no outlines. */
|
|
439
433
|
.assistant-transcript__question {
|
|
440
434
|
align-items: start;
|
|
441
|
-
background:
|
|
442
|
-
border: 1px solid rgba(var(--v-theme-outline), 0.2);
|
|
435
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
443
436
|
border-radius: 8px;
|
|
444
437
|
box-sizing: border-box;
|
|
445
438
|
display: grid;
|
|
@@ -452,10 +445,9 @@
|
|
|
452
445
|
|
|
453
446
|
.assistant-transcript__question-number {
|
|
454
447
|
align-items: center;
|
|
455
|
-
background:
|
|
456
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.2);
|
|
448
|
+
background: rgb(var(--v-theme-secondary-container));
|
|
457
449
|
border-radius: 999px;
|
|
458
|
-
color: rgb(var(--v-theme-
|
|
450
|
+
color: rgb(var(--v-theme-on-secondary-container));
|
|
459
451
|
display: inline-flex;
|
|
460
452
|
font-size: 0.72rem;
|
|
461
453
|
font-weight: 760;
|
|
@@ -44,10 +44,9 @@ function handleLinkClick(event, part = {}) {
|
|
|
44
44
|
|
|
45
45
|
<style scoped>
|
|
46
46
|
code {
|
|
47
|
-
background:
|
|
48
|
-
border: 1px solid rgba(var(--v-theme-primary), 0.14);
|
|
47
|
+
background: rgb(var(--v-theme-surface-container-highest));
|
|
49
48
|
border-radius: 3px;
|
|
50
|
-
color:
|
|
49
|
+
color: rgb(var(--v-theme-on-surface));
|
|
51
50
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
52
51
|
font-size: 0.9em;
|
|
53
52
|
overflow-wrap: anywhere;
|
|
@@ -240,9 +240,10 @@ h5.studio-long-text-review__heading {
|
|
|
240
240
|
font-size: 0.8rem;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/* Code, tables and details are Material 3 tonal containers (surface-container tones), not outlined boxes; a
|
|
244
|
+
table keeps outline-variant dividers between its rows. */
|
|
243
245
|
.studio-long-text-review__code {
|
|
244
|
-
background:
|
|
245
|
-
border: 1px solid rgba(var(--v-border-color), 0.26);
|
|
246
|
+
background: rgb(var(--v-theme-surface-container-highest));
|
|
246
247
|
border-radius: 8px;
|
|
247
248
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
248
249
|
font-size: 0.82rem;
|
|
@@ -256,7 +257,7 @@ h5.studio-long-text-review__heading {
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
.studio-long-text-review__table-wrap {
|
|
259
|
-
|
|
260
|
+
background: rgb(var(--v-theme-surface-container-low));
|
|
260
261
|
border-radius: 8px;
|
|
261
262
|
inline-size: fit-content;
|
|
262
263
|
margin-block: 0.35rem;
|
|
@@ -282,7 +283,7 @@ h5.studio-long-text-review__heading {
|
|
|
282
283
|
|
|
283
284
|
.studio-long-text-review__table th,
|
|
284
285
|
.studio-long-text-review__table td {
|
|
285
|
-
border-bottom: 1px solid
|
|
286
|
+
border-bottom: 1px solid rgb(var(--v-theme-outline-variant));
|
|
286
287
|
max-inline-size: 28rem;
|
|
287
288
|
padding: 0.55rem 0.75rem;
|
|
288
289
|
text-align: left;
|
|
@@ -290,7 +291,7 @@ h5.studio-long-text-review__heading {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
.studio-long-text-review__table th {
|
|
293
|
-
background:
|
|
294
|
+
background: rgb(var(--v-theme-surface-container-high));
|
|
294
295
|
color: rgb(var(--v-theme-on-surface));
|
|
295
296
|
font-weight: 760;
|
|
296
297
|
position: sticky;
|
|
@@ -320,7 +321,7 @@ h5.studio-long-text-review__heading {
|
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
.studio-long-text-review__details {
|
|
323
|
-
|
|
324
|
+
background: rgb(var(--v-theme-surface-container-low));
|
|
324
325
|
border-radius: 8px;
|
|
325
326
|
display: block;
|
|
326
327
|
min-width: 0;
|
|
@@ -550,7 +550,12 @@ export function createCodexAppServerProviderOwner({
|
|
|
550
550
|
assertOpen();
|
|
551
551
|
if (!record.observationStop || record.observationStop.error !== error) {
|
|
552
552
|
reportRecoveryEvent(record, "lost", error);
|
|
553
|
-
|
|
553
|
+
// A shared stop of this native runtime gives each of its peers the failure that started it, and records their
|
|
554
|
+
// barriers and completion itself. A peer that then loses its own connection to the stopped process joins that
|
|
555
|
+
// stop instead of stopping the runtime again, which would reopen its stopped barrier while the first finishes.
|
|
556
|
+
const runtimeKey = codexAppServerOwnedRuntimeKey(record.providerKey, record.providerOptions);
|
|
557
|
+
const shared = runtimePeers(runtimeKey).find(peer => peer !== record && peer.observationStop?.error === error);
|
|
558
|
+
const stop = { error, promise: shared?.observationStop.promise || invalidation.suspendUnobservedProvider(record, error) };
|
|
554
559
|
record.observationStop = stop;
|
|
555
560
|
const tracked = stop.promise.catch(failure => {
|
|
556
561
|
reportRecoveryEvent(record, "stopFailed", failure);
|
|
@@ -3,16 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
createChromiumLaunchOptions,
|
|
9
|
-
startViteFixture,
|
|
10
|
-
stopProcess
|
|
11
|
-
} from "../../../tooling/testUtils/browserFixture.mjs";
|
|
6
|
+
import { expect } from "../../../tooling/testUtils/browserExpect.mjs";
|
|
7
|
+
import { sharedBrowserFixture } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
12
8
|
|
|
13
9
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
14
10
|
const FIXTURE_ROOT = path.resolve(TEST_DIRECTORY, "../fixtures/responsive-assistant");
|
|
15
11
|
const RUN_BROWSER_TEST = process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION === "1";
|
|
12
|
+
// Every test reads the same fixture app, so the file shares one server and one Chromium; each test gets its own pages.
|
|
13
|
+
const openFixture = sharedBrowserFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
16
14
|
// Turns use content-visibility estimates. Moving the reader materializes rows
|
|
17
15
|
// near the new position, and Chromium's scroll anchoring then shifts scrollTop
|
|
18
16
|
// to keep the same content in view. Hold a reading position only once that
|
|
@@ -109,9 +107,8 @@ const VIEWPORTS = Object.freeze([
|
|
|
109
107
|
|
|
110
108
|
test("progress previews omit expandable details without hiding the current update", {
|
|
111
109
|
skip: !RUN_BROWSER_TEST, timeout: 60_000
|
|
112
|
-
}, async () => {
|
|
113
|
-
const vite = await
|
|
114
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
110
|
+
}, async (t) => {
|
|
111
|
+
const { vite, browser } = await openFixture(t);
|
|
115
112
|
try {
|
|
116
113
|
for (const width of [390, 800, 1365]) {
|
|
117
114
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -137,15 +134,13 @@ test("progress previews omit expandable details without hiding the current updat
|
|
|
137
134
|
}
|
|
138
135
|
} finally {
|
|
139
136
|
await browser.close();
|
|
140
|
-
await stopProcess(vite);
|
|
141
137
|
}
|
|
142
138
|
});
|
|
143
139
|
|
|
144
140
|
test("saved reply attribution remains when the current assistant changes", {
|
|
145
141
|
skip: !RUN_BROWSER_TEST, timeout: 60_000
|
|
146
|
-
}, async () => {
|
|
147
|
-
const vite = await
|
|
148
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
142
|
+
}, async (t) => {
|
|
143
|
+
const { vite, browser } = await openFixture(t);
|
|
149
144
|
try {
|
|
150
145
|
const page = await browser.newPage();
|
|
151
146
|
await page.goto(`${vite.baseURL}/?conversation=1&attribution=1`);
|
|
@@ -157,15 +152,13 @@ test("saved reply attribution remains when the current assistant changes", {
|
|
|
157
152
|
await expect(names).toHaveText(["First assistant", "Message assistant", "Next assistant"]);
|
|
158
153
|
} finally {
|
|
159
154
|
await browser.close();
|
|
160
|
-
await stopProcess(vite);
|
|
161
155
|
}
|
|
162
156
|
});
|
|
163
157
|
|
|
164
158
|
test("streaming answers grow in one bubble, preserve drafts and settle once at every width", {
|
|
165
159
|
skip: !RUN_BROWSER_TEST, timeout: 90_000
|
|
166
|
-
}, async () => {
|
|
167
|
-
const vite = await
|
|
168
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
160
|
+
}, async (t) => {
|
|
161
|
+
const { vite, browser } = await openFixture(t);
|
|
169
162
|
try {
|
|
170
163
|
for (const width of [390, 800, 1365]) {
|
|
171
164
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -189,16 +182,14 @@ test("streaming answers grow in one bubble, preserve drafts and settle once at e
|
|
|
189
182
|
}
|
|
190
183
|
} finally {
|
|
191
184
|
await browser.close();
|
|
192
|
-
await stopProcess(vite);
|
|
193
185
|
}
|
|
194
186
|
});
|
|
195
187
|
|
|
196
188
|
test("adjacent reasoning stays grouped across storage rows, history pages and execution changes", {
|
|
197
189
|
skip: !RUN_BROWSER_TEST,
|
|
198
190
|
timeout: 90_000
|
|
199
|
-
}, async () => {
|
|
200
|
-
const vite = await
|
|
201
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
191
|
+
}, async (t) => {
|
|
192
|
+
const { vite, browser } = await openFixture(t);
|
|
202
193
|
try {
|
|
203
194
|
for (const width of [390, 800, 1365]) {
|
|
204
195
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -262,7 +253,6 @@ test("adjacent reasoning stays grouped across storage rows, history pages and ex
|
|
|
262
253
|
}
|
|
263
254
|
} finally {
|
|
264
255
|
await browser.close();
|
|
265
|
-
await stopProcess(vite);
|
|
266
256
|
}
|
|
267
257
|
});
|
|
268
258
|
|
|
@@ -286,13 +276,11 @@ test("AssistantConversationElement keeps long conversations scrollable across re
|
|
|
286
276
|
skip: RUN_BROWSER_TEST
|
|
287
277
|
? false
|
|
288
278
|
: "set JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION=1 to run assistant-core browser integration",
|
|
289
|
-
timeout:
|
|
290
|
-
}, async () => {
|
|
291
|
-
const vite = await
|
|
292
|
-
let browser = null;
|
|
279
|
+
timeout: 90_000
|
|
280
|
+
}, async (t) => {
|
|
281
|
+
const { vite, browser } = await openFixture(t);
|
|
293
282
|
|
|
294
283
|
try {
|
|
295
|
-
browser = await chromium.launch(createChromiumLaunchOptions());
|
|
296
284
|
const context = await browser.newContext({
|
|
297
285
|
baseURL: vite.baseURL,
|
|
298
286
|
locale: "en-US"
|
|
@@ -315,16 +303,14 @@ test("AssistantConversationElement keeps long conversations scrollable across re
|
|
|
315
303
|
);
|
|
316
304
|
} finally {
|
|
317
305
|
await browser?.close();
|
|
318
|
-
await stopProcess(vite);
|
|
319
306
|
}
|
|
320
307
|
});
|
|
321
308
|
|
|
322
309
|
test("conversation scrolling preserves history anchors and resets expansion on selection", {
|
|
323
310
|
skip: !RUN_BROWSER_TEST,
|
|
324
311
|
timeout: 60_000
|
|
325
|
-
}, async () => {
|
|
326
|
-
const vite = await
|
|
327
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
312
|
+
}, async (t) => {
|
|
313
|
+
const { vite, browser } = await openFixture(t);
|
|
328
314
|
try {
|
|
329
315
|
const page = await browser.newPage();
|
|
330
316
|
await page.goto(`${vite.baseURL}/?controls=1`);
|
|
@@ -375,16 +361,14 @@ test("conversation scrolling preserves history anchors and resets expansion on s
|
|
|
375
361
|
assert.equal(await page.evaluate(() => window.revealPlaceholders), 0, "reopening loaded text does not flash a loading placeholder");
|
|
376
362
|
} finally {
|
|
377
363
|
await browser.close();
|
|
378
|
-
await stopProcess(vite);
|
|
379
364
|
}
|
|
380
365
|
});
|
|
381
366
|
|
|
382
367
|
test("a delayed final history page preserves the visible message at every width", {
|
|
383
368
|
skip: !RUN_BROWSER_TEST,
|
|
384
369
|
timeout: 60_000
|
|
385
|
-
}, async () => {
|
|
386
|
-
const vite = await
|
|
387
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
370
|
+
}, async (t) => {
|
|
371
|
+
const { vite, browser } = await openFixture(t);
|
|
388
372
|
try {
|
|
389
373
|
for (const width of [390, 800, 1365]) {
|
|
390
374
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -410,16 +394,14 @@ test("a delayed final history page preserves the visible message at every width"
|
|
|
410
394
|
}
|
|
411
395
|
} finally {
|
|
412
396
|
await browser.close();
|
|
413
|
-
await stopProcess(vite);
|
|
414
397
|
}
|
|
415
398
|
});
|
|
416
399
|
|
|
417
400
|
test("upward scrolling loads older history once and preserves retry and selection behavior", {
|
|
418
401
|
skip: !RUN_BROWSER_TEST,
|
|
419
402
|
timeout: 90_000
|
|
420
|
-
}, async () => {
|
|
421
|
-
const vite = await
|
|
422
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
403
|
+
}, async (t) => {
|
|
404
|
+
const { vite, browser } = await openFixture(t);
|
|
423
405
|
try {
|
|
424
406
|
for (const width of [390, 800, 1365]) {
|
|
425
407
|
const touch = width < 1000;
|
|
@@ -504,16 +486,14 @@ test("upward scrolling loads older history once and preserves retry and selectio
|
|
|
504
486
|
}
|
|
505
487
|
} finally {
|
|
506
488
|
await browser.close();
|
|
507
|
-
await stopProcess(vite);
|
|
508
489
|
}
|
|
509
490
|
});
|
|
510
491
|
|
|
511
492
|
test("a reader's own upward scrolling leaves the latest message from its first frame", {
|
|
512
493
|
skip: !RUN_BROWSER_TEST,
|
|
513
494
|
timeout: 60_000
|
|
514
|
-
}, async () => {
|
|
515
|
-
const vite = await
|
|
516
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
495
|
+
}, async (t) => {
|
|
496
|
+
const { vite, browser } = await openFixture(t);
|
|
517
497
|
try {
|
|
518
498
|
for (const width of [390, 1365]) {
|
|
519
499
|
const touch = width < 1000;
|
|
@@ -580,16 +560,14 @@ test("a reader's own upward scrolling leaves the latest message from its first f
|
|
|
580
560
|
}
|
|
581
561
|
} finally {
|
|
582
562
|
await browser.close();
|
|
583
|
-
await stopProcess(vite);
|
|
584
563
|
}
|
|
585
564
|
});
|
|
586
565
|
|
|
587
566
|
test("a reader moved away from the end without a scroll direction stays there", {
|
|
588
567
|
skip: !RUN_BROWSER_TEST,
|
|
589
568
|
timeout: 60_000
|
|
590
|
-
}, async () => {
|
|
591
|
-
const vite = await
|
|
592
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
569
|
+
}, async (t) => {
|
|
570
|
+
const { vite, browser } = await openFixture(t);
|
|
593
571
|
try {
|
|
594
572
|
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
595
573
|
const errors = [];
|
|
@@ -618,16 +596,14 @@ test("a reader moved away from the end without a scroll direction stays there",
|
|
|
618
596
|
assert.deepEqual(errors, []);
|
|
619
597
|
} finally {
|
|
620
598
|
await browser.close();
|
|
621
|
-
await stopProcess(vite);
|
|
622
599
|
}
|
|
623
600
|
});
|
|
624
601
|
|
|
625
602
|
test("retained hidden transcripts preserve readers and follow only at the latest message", {
|
|
626
603
|
skip: !RUN_BROWSER_TEST,
|
|
627
604
|
timeout: 90_000
|
|
628
|
-
}, async () => {
|
|
629
|
-
const vite = await
|
|
630
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
605
|
+
}, async (t) => {
|
|
606
|
+
const { vite, browser } = await openFixture(t);
|
|
631
607
|
try {
|
|
632
608
|
for (const width of [390, 800, 1365]) {
|
|
633
609
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -683,16 +659,14 @@ test("retained hidden transcripts preserve readers and follow only at the latest
|
|
|
683
659
|
}
|
|
684
660
|
} finally {
|
|
685
661
|
await browser.close();
|
|
686
|
-
await stopProcess(vite);
|
|
687
662
|
}
|
|
688
663
|
});
|
|
689
664
|
|
|
690
665
|
test("transient update errors keep the loaded transcript, reader, draft and focus in a compact banner", {
|
|
691
666
|
skip: !RUN_BROWSER_TEST,
|
|
692
667
|
timeout: 90_000
|
|
693
|
-
}, async () => {
|
|
694
|
-
const vite = await
|
|
695
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
668
|
+
}, async (t) => {
|
|
669
|
+
const { vite, browser } = await openFixture(t);
|
|
696
670
|
try {
|
|
697
671
|
for (const width of [390, 800, 1365]) {
|
|
698
672
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -750,16 +724,14 @@ test("transient update errors keep the loaded transcript, reader, draft and focu
|
|
|
750
724
|
}
|
|
751
725
|
} finally {
|
|
752
726
|
await browser.close();
|
|
753
|
-
await stopProcess(vite);
|
|
754
727
|
}
|
|
755
728
|
});
|
|
756
729
|
|
|
757
730
|
test("retained hidden history loads restore their original anchor and invalidate retired requests", {
|
|
758
731
|
skip: !RUN_BROWSER_TEST,
|
|
759
732
|
timeout: 90_000
|
|
760
|
-
}, async () => {
|
|
761
|
-
const vite = await
|
|
762
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
733
|
+
}, async (t) => {
|
|
734
|
+
const { vite, browser } = await openFixture(t);
|
|
763
735
|
try {
|
|
764
736
|
for (const width of [390, 800, 1365]) {
|
|
765
737
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -818,16 +790,14 @@ test("retained hidden history loads restore their original anchor and invalidate
|
|
|
818
790
|
}
|
|
819
791
|
} finally {
|
|
820
792
|
await browser.close();
|
|
821
|
-
await stopProcess(vite);
|
|
822
793
|
}
|
|
823
794
|
});
|
|
824
795
|
|
|
825
796
|
test("conversation composer preserves typing and reacts to external state and pane resizing", {
|
|
826
797
|
skip: !RUN_BROWSER_TEST,
|
|
827
798
|
timeout: 60_000
|
|
828
|
-
}, async () => {
|
|
829
|
-
const vite = await
|
|
830
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
799
|
+
}, async (t) => {
|
|
800
|
+
const { vite, browser } = await openFixture(t);
|
|
831
801
|
try {
|
|
832
802
|
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
833
803
|
await page.goto(`${vite.baseURL}/?conversation=1`);
|
|
@@ -872,16 +842,14 @@ test("conversation composer preserves typing and reacts to external state and pa
|
|
|
872
842
|
}
|
|
873
843
|
} finally {
|
|
874
844
|
await browser.close();
|
|
875
|
-
await stopProcess(vite);
|
|
876
845
|
}
|
|
877
846
|
});
|
|
878
847
|
|
|
879
848
|
test("working state appears before output and suggestion agents and activity rendering are replaceable", {
|
|
880
849
|
skip: !RUN_BROWSER_TEST,
|
|
881
850
|
timeout: 60_000
|
|
882
|
-
}, async () => {
|
|
883
|
-
const vite = await
|
|
884
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
851
|
+
}, async (t) => {
|
|
852
|
+
const { vite, browser } = await openFixture(t);
|
|
885
853
|
try {
|
|
886
854
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
887
855
|
await page.goto(`${vite.baseURL}/?conversation=1&support=1`);
|
|
@@ -910,7 +878,6 @@ test("working state appears before output and suggestion agents and activity ren
|
|
|
910
878
|
await expect(input).toHaveValue("Keep typing while waiting");
|
|
911
879
|
} finally {
|
|
912
880
|
await browser.close();
|
|
913
|
-
await stopProcess(vite);
|
|
914
881
|
}
|
|
915
882
|
});
|
|
916
883
|
|
|
@@ -918,9 +885,8 @@ test("working state appears before output and suggestion agents and activity ren
|
|
|
918
885
|
test("long goal objectives keep Start reachable on short phone, tablet and desktop screens", {
|
|
919
886
|
skip: !RUN_BROWSER_TEST,
|
|
920
887
|
timeout: 60_000
|
|
921
|
-
}, async () => {
|
|
922
|
-
const vite = await
|
|
923
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
888
|
+
}, async (t) => {
|
|
889
|
+
const { vite, browser } = await openFixture(t);
|
|
924
890
|
try {
|
|
925
891
|
const objective = "Complete the requested work and verify the result. ".repeat(80).slice(0, 3990);
|
|
926
892
|
for (const width of [390, 800, 1280]) {
|
|
@@ -947,16 +913,14 @@ test("long goal objectives keep Start reachable on short phone, tablet and deskt
|
|
|
947
913
|
}
|
|
948
914
|
} finally {
|
|
949
915
|
await browser.close();
|
|
950
|
-
await stopProcess(vite);
|
|
951
916
|
}
|
|
952
917
|
});
|
|
953
918
|
|
|
954
919
|
test("optional goals show running and paused lights, count only active time, and adapt to pane width", {
|
|
955
920
|
skip: !RUN_BROWSER_TEST,
|
|
956
921
|
timeout: 60_000
|
|
957
|
-
}, async () => {
|
|
958
|
-
const vite = await
|
|
959
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
922
|
+
}, async (t) => {
|
|
923
|
+
const { vite, browser } = await openFixture(t);
|
|
960
924
|
try {
|
|
961
925
|
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
962
926
|
await page.clock.install();
|
|
@@ -1035,7 +999,6 @@ test("optional goals show running and paused lights, count only active time, and
|
|
|
1035
999
|
await expect(input).toHaveValue("Preserve this draft and caret");
|
|
1036
1000
|
} finally {
|
|
1037
1001
|
await browser.close();
|
|
1038
|
-
await stopProcess(vite);
|
|
1039
1002
|
}
|
|
1040
1003
|
});
|
|
1041
1004
|
|
|
@@ -1043,9 +1006,8 @@ test("optional goals show running and paused lights, count only active time, and
|
|
|
1043
1006
|
test("shared model choices and file delivery preserve a responsive composer", {
|
|
1044
1007
|
skip: !RUN_BROWSER_TEST,
|
|
1045
1008
|
timeout: 60_000
|
|
1046
|
-
}, async () => {
|
|
1047
|
-
const vite = await
|
|
1048
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1009
|
+
}, async (t) => {
|
|
1010
|
+
const { vite, browser } = await openFixture(t);
|
|
1049
1011
|
try {
|
|
1050
1012
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
1051
1013
|
await page.goto(`${vite.baseURL}/?conversation=1&capabilities=1`);
|
|
@@ -1084,15 +1046,13 @@ test("shared model choices and file delivery preserve a responsive composer", {
|
|
|
1084
1046
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
1085
1047
|
} finally {
|
|
1086
1048
|
await browser.close();
|
|
1087
|
-
await stopProcess(vite);
|
|
1088
1049
|
}
|
|
1089
1050
|
});
|
|
1090
1051
|
|
|
1091
1052
|
test("an optional avatar clamps in a short conversation without consuming its preference or draft", {
|
|
1092
1053
|
skip: !RUN_BROWSER_TEST, timeout: 90_000
|
|
1093
|
-
}, async () => {
|
|
1094
|
-
const vite = await
|
|
1095
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1054
|
+
}, async (t) => {
|
|
1055
|
+
const { vite, browser } = await openFixture(t);
|
|
1096
1056
|
try {
|
|
1097
1057
|
for (const width of [390, 800, 1365]) {
|
|
1098
1058
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -1122,8 +1082,7 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
1122
1082
|
assert.ok(send.y + send.height <= bounds.y + bounds.height + 1, "Send stays inside the supplied container");
|
|
1123
1083
|
assert.ok(feedback.y + feedback.height <= bounds.y + bounds.height + 1, "Feedback stays inside the supplied container");
|
|
1124
1084
|
assert.ok((await input.boundingBox()).height >= 40);
|
|
1125
|
-
|
|
1126
|
-
assert.equal(avatarButton.width, avatarExpanded ? 40 : 44, "Expanded minimise is compact; collapsed Show retains its wider target");
|
|
1085
|
+
assert.equal(Math.round(avatarButton.width), 40, "Avatar visibility is a 40dp icon button, Show and Minimise alike");
|
|
1127
1086
|
assert.equal(Math.round(avatarButton.height), 40, "Avatar visibility is a 40dp icon button, with no fixed height");
|
|
1128
1087
|
const avatarTarget = await sizeControl.evaluate((element) => Number.parseFloat(window.getComputedStyle(element, "::before").height));
|
|
1129
1088
|
assert.ok(avatarTarget >= 48, "Avatar visibility keeps Material's 48dp touch target");
|
|
@@ -1175,7 +1134,7 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
1175
1134
|
await expect(artwork).toHaveCount(0);
|
|
1176
1135
|
await expect(sizeControl).toBeVisible();
|
|
1177
1136
|
const showBounds = await sizeControl.boundingBox();
|
|
1178
|
-
assert.equal(showBounds.width,
|
|
1137
|
+
assert.equal(Math.round(showBounds.width), 40, "Collapsed Show is the small icon button, as Minimise is");
|
|
1179
1138
|
assert.equal(Math.round(showBounds.height), 40, "Collapsed Show keeps the small icon button's height");
|
|
1180
1139
|
assert.ok(await sizeControl.evaluate(element => Number.parseFloat(getComputedStyle(element, "::before").height)) >= 48,
|
|
1181
1140
|
"Collapsed Show keeps a 48dp touch target");
|
|
@@ -1249,16 +1208,14 @@ test("an optional avatar clamps in a short conversation without consuming its pr
|
|
|
1249
1208
|
}
|
|
1250
1209
|
} finally {
|
|
1251
1210
|
await browser.close();
|
|
1252
|
-
await stopProcess(vite);
|
|
1253
1211
|
}
|
|
1254
1212
|
});
|
|
1255
1213
|
|
|
1256
1214
|
|
|
1257
1215
|
test("unsent preview edits stay in their bubble and disappear for the canonical message", {
|
|
1258
1216
|
skip: !RUN_BROWSER_TEST, timeout: 60_000
|
|
1259
|
-
}, async () => {
|
|
1260
|
-
const vite = await
|
|
1261
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1217
|
+
}, async (t) => {
|
|
1218
|
+
const { vite, browser } = await openFixture(t);
|
|
1262
1219
|
try {
|
|
1263
1220
|
const page = await browser.newPage({ viewport: { width: 320, height: 844 } });
|
|
1264
1221
|
await page.goto(`${vite.baseURL}/?conversation=1&preview=1`);
|
|
@@ -1363,7 +1320,6 @@ test("unsent preview edits stay in their bubble and disappear for the canonical
|
|
|
1363
1320
|
}
|
|
1364
1321
|
} finally {
|
|
1365
1322
|
await browser.close();
|
|
1366
|
-
await stopProcess(vite);
|
|
1367
1323
|
}
|
|
1368
1324
|
});
|
|
1369
1325
|
|
|
@@ -1371,9 +1327,8 @@ test("unsent preview edits stay in their bubble and disappear for the canonical
|
|
|
1371
1327
|
test("pending history keeps following the reader when a new reply changes transcript height", {
|
|
1372
1328
|
skip: !RUN_BROWSER_TEST,
|
|
1373
1329
|
timeout: 90_000
|
|
1374
|
-
}, async () => {
|
|
1375
|
-
const vite = await
|
|
1376
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1330
|
+
}, async (t) => {
|
|
1331
|
+
const { vite, browser } = await openFixture(t);
|
|
1377
1332
|
try {
|
|
1378
1333
|
for (const width of [390, 1365]) {
|
|
1379
1334
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -1414,16 +1369,14 @@ test("pending history keeps following the reader when a new reply changes transc
|
|
|
1414
1369
|
}
|
|
1415
1370
|
} finally {
|
|
1416
1371
|
await browser.close();
|
|
1417
|
-
await stopProcess(vite);
|
|
1418
1372
|
}
|
|
1419
1373
|
});
|
|
1420
1374
|
|
|
1421
1375
|
|
|
1422
1376
|
test("connection warnings keep avatar controls and Reload reachable without replacing history", {
|
|
1423
1377
|
skip: !RUN_BROWSER_TEST, timeout: 90_000
|
|
1424
|
-
}, async () => {
|
|
1425
|
-
const vite = await
|
|
1426
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1378
|
+
}, async (t) => {
|
|
1379
|
+
const { vite, browser } = await openFixture(t);
|
|
1427
1380
|
try {
|
|
1428
1381
|
for (const width of [320, 800]) {
|
|
1429
1382
|
const page = await browser.newPage({ viewport: { width, height: 1000 } });
|
|
@@ -1498,16 +1451,14 @@ test("connection warnings keep avatar controls and Reload reachable without repl
|
|
|
1498
1451
|
}
|
|
1499
1452
|
} finally {
|
|
1500
1453
|
await browser.close();
|
|
1501
|
-
await stopProcess(vite);
|
|
1502
1454
|
}
|
|
1503
1455
|
});
|
|
1504
1456
|
|
|
1505
1457
|
|
|
1506
1458
|
test("short connection warnings preserve full text and recovery targets at phone widths", {
|
|
1507
1459
|
skip: !RUN_BROWSER_TEST, timeout: 90_000
|
|
1508
|
-
}, async () => {
|
|
1509
|
-
const vite = await
|
|
1510
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
1460
|
+
}, async (t) => {
|
|
1461
|
+
const { vite, browser } = await openFixture(t);
|
|
1511
1462
|
try {
|
|
1512
1463
|
for (const width of [320, 390]) {
|
|
1513
1464
|
const page = await browser.newPage({ viewport: { width, height: 768 } });
|
|
@@ -1580,6 +1531,5 @@ test("short connection warnings preserve full text and recovery targets at phone
|
|
|
1580
1531
|
}
|
|
1581
1532
|
} finally {
|
|
1582
1533
|
await browser.close();
|
|
1583
|
-
await stopProcess(vite);
|
|
1584
1534
|
}
|
|
1585
1535
|
});
|
|
@@ -759,7 +759,9 @@ test("Claude cancellation waits for an application effect after closing the nati
|
|
|
759
759
|
});
|
|
760
760
|
|
|
761
761
|
test("the common request deadline stops an unfinished native turn and retains its admitted history", async t => {
|
|
762
|
-
|
|
762
|
+
// The deadline also covers starting the native process and admitting the turn, which took up to 8.4 s (median 1.3 to
|
|
763
|
+
// 2.5 s) with 4 to 8 copies of this test running on an 8-thread host, so it allows 10 s.
|
|
764
|
+
const f = await fixture(t, { limits: { timeoutMs: 10_000 } });
|
|
763
765
|
await f.conversation.send({ messageId: "waiting", text: "wait" });
|
|
764
766
|
const snapshot = await f.conversation.wait();
|
|
765
767
|
assert.equal(snapshot.conversationLog[0].metadata.runtime.status, "failed");
|
|
@@ -11,3 +11,47 @@ test("client entry points expose one conversation implementation and retain sett
|
|
|
11
11
|
const conversation = await readFile(new URL("../src/client/conversation/index.js", import.meta.url), "utf8");
|
|
12
12
|
assert.match(conversation, /AssistantConversationElement/);
|
|
13
13
|
});
|
|
14
|
+
|
|
15
|
+
// Material 3: one container at most, the composer an outlined text field, and no heading inside a titled pane.
|
|
16
|
+
test("the transcript, composer and settings card each draw one Material 3 container", async () => {
|
|
17
|
+
const transcript = await readFile(new URL("../src/client/conversation/AssistantTranscript.css", import.meta.url), "utf8");
|
|
18
|
+
const block = (css, selector) => new RegExp(`(?:^|\\n)${selector.replaceAll(".", "\\.")} \\{([^}]*)\\}`, "u").exec(css)?.[1] ?? "";
|
|
19
|
+
assert.doesNotMatch(block(transcript, ".assistant-transcript"), /border/u, "the transcript area is not outlined");
|
|
20
|
+
assert.doesNotMatch(block(transcript, ".assistant-transcript__welcome"), /\bborder:/u, "the welcome bubble is a tonal fill only");
|
|
21
|
+
const prompt = await readFile(new URL("../src/client/conversation/AssistantPromptInput.vue", import.meta.url), "utf8");
|
|
22
|
+
const field = block(prompt, ".assistant-prompt-input__field");
|
|
23
|
+
assert.match(field, /border: 1px solid rgb\(var\(--v-theme-outline\)\);/u);
|
|
24
|
+
assert.match(field, /border-radius: 4px;/u);
|
|
25
|
+
assert.match(block(prompt, ".assistant-prompt-input__footer"), /padding: 0 12px 12px;/u);
|
|
26
|
+
const card = await readFile(new URL("../src/client/components/AssistantSettingsFormCard.vue", import.meta.url), "utf8");
|
|
27
|
+
assert.doesNotMatch(card, /<v-card-title|<v-card-subtitle|<v-divider/u);
|
|
28
|
+
assert.match(card, /:aria-label="props\.title"/u);
|
|
29
|
+
// Given a formId, the page's app bar carries Save: the form takes the id and the card draws no Save of its own.
|
|
30
|
+
assert.match(card, /<v-form :id="props\.formId \|\| undefined"/u);
|
|
31
|
+
assert.match(card, /<div v-if="!props\.formId \|\| !props\.addEdit\.canSave"/u);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Material 3 tonal containers: inside the conversation only the composer, an outlined text field, draws an outline;
|
|
35
|
+
// attachments, questions, code, tables, details and the model menu are surface-container tones.
|
|
36
|
+
test("the conversation's containers are surface-container tones, not outlined boxes", async () => {
|
|
37
|
+
const folder = new URL("../src/client/conversation/", import.meta.url);
|
|
38
|
+
const files = ["AssistantTranscript.css", "AssistantAttachmentQueue.vue", "AssistantMessageAttachments.vue",
|
|
39
|
+
"AssistantModelControl.vue", "LongTextPreviewBlocks.vue", "LongTextInlineParts.vue", "AssistantPromptInput.vue"];
|
|
40
|
+
const outlines = [];
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const source = await readFile(new URL(file, folder), "utf8");
|
|
43
|
+
for (const [declaration] of source.matchAll(/\bborder(?:-(?:top|bottom|left|right|inline|block)(?:-(?:start|end))?)?: [^;]*\d[^;]*;/gu)) {
|
|
44
|
+
if (!/: 0;/u.test(declaration)) outlines.push(`${file}: ${declaration}`);
|
|
45
|
+
}
|
|
46
|
+
if (file === "AssistantModelControl.vue") {
|
|
47
|
+
assert.doesNotMatch(source, /<v-sheet[^>]*\n\s*border\n/u, "the model menu has no outline");
|
|
48
|
+
assert.match(source, /color="surface-container"\s+:elevation="2"/u);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
assert.deepEqual(outlines, [
|
|
52
|
+
"AssistantModelControl.vue: border-bottom: 1px solid rgb(var(--v-theme-outline-variant));",
|
|
53
|
+
"AssistantModelControl.vue: border-top: 1px solid rgb(var(--v-theme-outline-variant));",
|
|
54
|
+
"LongTextPreviewBlocks.vue: border-bottom: 1px solid rgb(var(--v-theme-outline-variant));",
|
|
55
|
+
"AssistantPromptInput.vue: border: 1px solid rgb(var(--v-theme-outline));"
|
|
56
|
+
], "only outline-variant dividers and the composer's outlined field draw lines");
|
|
57
|
+
});
|
|
@@ -40,7 +40,6 @@ async function renderShowAvatarButton(element, slots) {
|
|
|
40
40
|
assert.equal(buttons.length, 1, "One Show avatar button while the avatar is hidden");
|
|
41
41
|
const [, attributes, content] = buttons[0];
|
|
42
42
|
assert.match(attributes, /title="jskit.assistant.chat.showAvatar"/u);
|
|
43
|
-
assert.match(attributes, /width="44"/u);
|
|
44
43
|
return content;
|
|
45
44
|
}
|
|
46
45
|
|
|
@@ -61,10 +60,10 @@ test("the hidden avatar's Show button shows the avatar-thumbnail slot, or the ac
|
|
|
61
60
|
assert.doesNotMatch(fallback, /fixture-thumbnail/u);
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
test("the avatar's Show and Minimise buttons set no fixed
|
|
63
|
+
test("the avatar's Show and Minimise buttons set no fixed size, so material-web gives them the 48dp touch target", () => {
|
|
65
64
|
const source = readFileSync(new URL(elementUrl), "utf8");
|
|
66
65
|
const buttons = [...source.matchAll(/<v-btn\b[^>]*>/gu)].map(([tag]) => tag).filter((tag) => tag.includes("assistant-conversation__avatar-size"));
|
|
67
66
|
assert.equal(buttons.length, 2);
|
|
68
|
-
for (const tag of buttons) assert.doesNotMatch(tag, /(?:^|\s):?(?:min-)?height=/u);
|
|
69
|
-
assert.doesNotMatch(source, /\.assistant-conversation__avatar-size\s*\{[^}]*height/u);
|
|
67
|
+
for (const tag of buttons) assert.doesNotMatch(tag, /(?:^|\s):?(?:min-)?(?:height|width)=/u);
|
|
68
|
+
assert.doesNotMatch(source, /\.assistant-conversation__avatar-size\s*\{[^}]*(?:height|width)/u);
|
|
70
69
|
});
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { expect } from "../../../tooling/testUtils/browserExpect.mjs";
|
|
5
|
+
import { sharedBrowserFixture } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
6
|
+
|
|
7
|
+
const openFixture = sharedBrowserFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/responsive-assistant", import.meta.url)) });
|
|
6
8
|
|
|
7
9
|
test("the shared element renders delayed sends and supplies retry, edit and cancel without application handlers", {
|
|
8
10
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
9
|
-
}, async () => {
|
|
10
|
-
const vite = await
|
|
11
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
11
|
+
}, async (t) => {
|
|
12
|
+
const { vite, browser } = await openFixture(t);
|
|
12
13
|
try {
|
|
13
14
|
for (const width of [390, 800, 1365]) {
|
|
14
15
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -102,16 +103,14 @@ test("the shared element renders delayed sends and supplies retry, edit and canc
|
|
|
102
103
|
}
|
|
103
104
|
} finally {
|
|
104
105
|
await browser.close();
|
|
105
|
-
await stopProcess(vite);
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
test("chronological messages and Cancel during a delivery check work on phone and desktop", {
|
|
111
111
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
112
|
-
}, async () => {
|
|
113
|
-
const vite = await
|
|
114
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
112
|
+
}, async (t) => {
|
|
113
|
+
const { vite, browser } = await openFixture(t);
|
|
115
114
|
try {
|
|
116
115
|
for (const width of [390, 1365]) {
|
|
117
116
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -148,14 +147,13 @@ test("chronological messages and Cancel during a delivery check work on phone an
|
|
|
148
147
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
149
148
|
await page.close();
|
|
150
149
|
}
|
|
151
|
-
} finally { await browser.close();
|
|
150
|
+
} finally { await browser.close(); }
|
|
152
151
|
});
|
|
153
152
|
|
|
154
153
|
test("accepted local messages precede later replies before their history page arrives", {
|
|
155
154
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
156
|
-
}, async () => {
|
|
157
|
-
const vite = await
|
|
158
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
155
|
+
}, async (t) => {
|
|
156
|
+
const { vite, browser } = await openFixture(t);
|
|
159
157
|
try {
|
|
160
158
|
for (const width of [390, 1365]) {
|
|
161
159
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -178,5 +176,5 @@ test("accepted local messages precede later replies before their history page ar
|
|
|
178
176
|
assert.deepEqual(errors, []);
|
|
179
177
|
await page.close();
|
|
180
178
|
}
|
|
181
|
-
} finally { await browser.close();
|
|
179
|
+
} finally { await browser.close(); }
|
|
182
180
|
});
|
|
@@ -77,6 +77,7 @@ async function fixture(t, options = {}) {
|
|
|
77
77
|
const directory = await mkdtemp(path.join(tmpdir(), "jskit-opencode-conversation-"));
|
|
78
78
|
const command = path.join(directory, "opencode.mjs");
|
|
79
79
|
const trace = path.join(directory, "trace.jsonl");
|
|
80
|
+
const partialSeen = path.join(directory, "partial-seen");
|
|
80
81
|
await writeFile(command, `#!${process.execPath}
|
|
81
82
|
import { createServer } from "node:http";
|
|
82
83
|
import { appendFileSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
@@ -90,6 +91,7 @@ async function fixture(t, options = {}) {
|
|
|
90
91
|
const streams = new Set();
|
|
91
92
|
const assistantResponses = ${JSON.stringify(options.assistantResponses || [])};
|
|
92
93
|
const busy = new Set();
|
|
94
|
+
const heldAnswers = new Map();
|
|
93
95
|
let sequence = [...sessions.values()].reduce((sum, session) => sum + session.messages.length, 0);
|
|
94
96
|
const row = (role, id, parts, extra = {}) => ({ info: { role, id, time: { created: ++sequence }, ...extra }, parts });
|
|
95
97
|
const server = createServer(async (req, res) => {
|
|
@@ -132,6 +134,12 @@ async function fixture(t, options = {}) {
|
|
|
132
134
|
}
|
|
133
135
|
if (req.url.includes("/message")) {
|
|
134
136
|
if (${Boolean(options.historyUnavailable)}) return reply({ message: "Native history unavailable" }, 503);
|
|
137
|
+
// A held answer completes at the first read after the test saw its partial streamed, or after 10 s, so a
|
|
138
|
+
// partial that never streams fails the test instead of hanging it.
|
|
139
|
+
const held = heldAnswers.get(session.id);
|
|
140
|
+
if (held && (existsSync(${JSON.stringify(partialSeen)}) || Date.now() - held.since > 10_000)) {
|
|
141
|
+
heldAnswers.delete(session.id); held.complete();
|
|
142
|
+
}
|
|
135
143
|
return reply(session.messages);
|
|
136
144
|
}
|
|
137
145
|
if (req.url.endsWith("/prompt_async")) {
|
|
@@ -217,11 +225,13 @@ async function fixture(t, options = {}) {
|
|
|
217
225
|
log({ system: output.system, sessionId: session.id });
|
|
218
226
|
const answer = row("assistant", randomUUID(), [{ type: "reasoning", text: "Reasoning summary" }, { type: "text", text: "Answer: " }]);
|
|
219
227
|
session.messages.push(answer); save();
|
|
220
|
-
|
|
228
|
+
const complete = () => {
|
|
221
229
|
answer.parts[1].text = answerText;
|
|
222
230
|
answer.info.finish = "stop"; answer.info.time.completed = Date.now(); save();
|
|
223
231
|
busy.delete(session.id);
|
|
224
|
-
}
|
|
232
|
+
};
|
|
233
|
+
if (${Boolean(options.holdPartialAnswer)}) heldAnswers.set(session.id, { complete, since: Date.now() });
|
|
234
|
+
else setTimeout(complete, 100);
|
|
225
235
|
return;
|
|
226
236
|
}
|
|
227
237
|
reply({ message: "Unexpected route" }, 404);
|
|
@@ -259,6 +269,7 @@ async function fixture(t, options = {}) {
|
|
|
259
269
|
limits: { admissionTimeoutMs: 250, ...options.limits }
|
|
260
270
|
},
|
|
261
271
|
binding: () => storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding),
|
|
272
|
+
releasePartialAnswer: () => writeFile(partialSeen, ""),
|
|
262
273
|
async trace() { return (await readFile(trace, "utf8")).trim().split("\n").map(line => JSON.parse(line)); } };
|
|
263
274
|
}
|
|
264
275
|
|
|
@@ -321,9 +332,14 @@ test("OpenCode structured output counts its schema framing before native dispatc
|
|
|
321
332
|
});
|
|
322
333
|
|
|
323
334
|
test("OpenCode admits once, streams and saves native reasoning through the common runtime", async t => {
|
|
324
|
-
|
|
335
|
+
// OpenCode text streams only through the runtime's history reads, so the native answer stays partial until this
|
|
336
|
+
// subscriber has seen it streamed, instead of for a fixed 100 ms that a read under load could miss.
|
|
337
|
+
const f = await fixture(t, { holdPartialAnswer: true });
|
|
325
338
|
const events = [];
|
|
326
|
-
await f.conversation.subscribe(event =>
|
|
339
|
+
await f.conversation.subscribe(event => {
|
|
340
|
+
events.push(event);
|
|
341
|
+
if (event.type === "message" && event.text === "Answer: ") void f.releasePartialAnswer();
|
|
342
|
+
});
|
|
327
343
|
assert.equal((await f.conversation.send(input)).status, "accepted");
|
|
328
344
|
const state = await f.conversation.wait();
|
|
329
345
|
assert.equal(state.conversationLog[0].metadata.runtime.status, "complete");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { expect } from "@playwright/test";
|
|
5
|
+
import { sharedBrowserFixture } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
6
6
|
|
|
7
7
|
// An element's text colour, the theme values it should come from, and its WCAG contrast over what is painted under
|
|
8
8
|
// it: each translucent background is laid over the next one out, down to the first opaque one (white when none is).
|
|
@@ -43,11 +43,12 @@ function readText(element) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const openFixture = sharedBrowserFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/responsive-assistant", import.meta.url)) });
|
|
47
|
+
|
|
46
48
|
test("the task transcript's user bubble, header time and footer follow the light and dark themes", {
|
|
47
49
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
48
|
-
}, async () => {
|
|
49
|
-
const vite = await
|
|
50
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
50
|
+
}, async (t) => {
|
|
51
|
+
const { vite, browser } = await openFixture(t);
|
|
51
52
|
try {
|
|
52
53
|
for (const theme of ["light", "dark"]) {
|
|
53
54
|
const page = await browser.newPage({ viewport: { width: 800, height: 900 } });
|
|
@@ -84,6 +85,5 @@ test("the task transcript's user bubble, header time and footer follow the light
|
|
|
84
85
|
}
|
|
85
86
|
} finally {
|
|
86
87
|
await browser.close();
|
|
87
|
-
await stopProcess(vite);
|
|
88
88
|
}
|
|
89
89
|
});
|