@jskit-ai/assistant-core 0.1.202 → 0.1.203
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/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/ConversationFixture.vue +30 -5
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +88 -3
- package/src/server/conversation/providers/claudeDriver.js +4 -0
- package/test/assistantScroll.browser.test.js +104 -0
- package/test/claudeConversationSession.test.js +28 -0
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.203",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.223",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.225",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.42",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.42"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -4,12 +4,22 @@ import AssistantConversationElement from "../../src/client/conversation/Assistan
|
|
|
4
4
|
import { useAssistantAttachments } from "../../src/client/conversation/useAssistantAttachments.js";
|
|
5
5
|
import { useAssistantSuggestions } from "../../src/client/conversation/useAssistantSuggestions.js";
|
|
6
6
|
const phase = ref("idle");
|
|
7
|
+
const avatarFixture = new URLSearchParams(location.search).has("avatar");
|
|
8
|
+
const showAvatar = ref(avatarFixture);
|
|
9
|
+
const avatarSize = ref("compact");
|
|
10
|
+
const short = ref(avatarFixture);
|
|
11
|
+
const showQuestions = ref(false);
|
|
12
|
+
const questionState = reactive({
|
|
13
|
+
questions: [{ name: "next", number: 1, label: "What should happen next?", choices: [] }], answers: {},
|
|
14
|
+
setAnswers(value) { questionState.answers = value; },
|
|
15
|
+
dismiss() { showQuestions.value = false; }
|
|
16
|
+
});
|
|
7
17
|
const attribution = new URLSearchParams(location.search).has("attribution");
|
|
8
18
|
const assistantLabel = ref("Current assistant");
|
|
9
19
|
const draft = ref("");
|
|
10
20
|
const narrow = ref(false);
|
|
11
21
|
const submissions = ref(0);
|
|
12
|
-
const feedback = ref(
|
|
22
|
+
const feedback = ref(avatarFixture);
|
|
13
23
|
const supportEnabled = new URLSearchParams(location.search).has("support");
|
|
14
24
|
const suggestionModel = ref("small-model");
|
|
15
25
|
const customActivity = ref(false);
|
|
@@ -61,13 +71,17 @@ const adapter = reactive({
|
|
|
61
71
|
{ turnId: "two", assistantLabel: "Second assistant", assistantDetails: "Second model",
|
|
62
72
|
assistant: { role: "assistant", text: "Second answer", assistantLabel: "Message assistant", assistantDetails: "Message model" } },
|
|
63
73
|
{ turnId: "three", assistant: { role: "assistant", text: "Unattributed answer" } }
|
|
64
|
-
] :
|
|
74
|
+
] : avatarFixture ? Array.from({ length: 20 }, (_, index) => ({
|
|
75
|
+
turnId: `help-${index}`, user: { messageId: `question-${index}`, text: `Support question ${index + 1}: How can I recover my account?` },
|
|
76
|
+
assistant: { messageId: `answer-${index}`, outputId: `output-${index}`, role: "assistant",
|
|
77
|
+
text: `Support answer ${index + 1}: Keep your recovery email available.\n\nRead the **account instructions** and follow the [recovery link](https://example.com/help). A long message wraps inside this narrow conversation without taking space from the composer.` }
|
|
78
|
+
})) : [],
|
|
65
79
|
visible: true, scrollKey: "fixture", welcomeMessage: "Composer responsiveness fixture"
|
|
66
80
|
},
|
|
67
81
|
composer: {
|
|
68
82
|
draft,
|
|
69
83
|
rows: 2,
|
|
70
|
-
density: computed(() => narrow.value ? "compact" : "default"),
|
|
84
|
+
density: computed(() => narrow.value || avatarFixture ? "compact" : "default"),
|
|
71
85
|
submitOnEnter: true,
|
|
72
86
|
canSend: computed(() => Boolean(draft.value.trim()) && ["idle", "active", "stopped"].includes(phase.value)),
|
|
73
87
|
canStop: computed(() => ["active", "stopping"].includes(phase.value)),
|
|
@@ -77,6 +91,7 @@ const adapter = reactive({
|
|
|
77
91
|
suggestions, goal: goalState,
|
|
78
92
|
attachments: capabilitiesEnabled ? attachments : undefined,
|
|
79
93
|
models: capabilitiesEnabled ? models : undefined,
|
|
94
|
+
questions: computed(() => showQuestions.value ? questionState : undefined),
|
|
80
95
|
actions: {
|
|
81
96
|
setDraft(value) { draft.value = value; },
|
|
82
97
|
submit(payload) { attachmentReceipts.value = payload.attachments; attachments.clearAttachments(); submissions.value += 1; draft.value = ""; phase.value = "active"; },
|
|
@@ -96,11 +111,19 @@ const adapter = reactive({
|
|
|
96
111
|
<button v-if="supportEnabled" @click="customActivity = !customActivity">Custom activity</button>
|
|
97
112
|
<button v-if="capabilitiesEnabled" @click="finishUploads">Finish uploads</button>
|
|
98
113
|
<button @click="goalEnabled = !goalEnabled">Toggle goals</button>
|
|
114
|
+
<template v-if="avatarFixture">
|
|
115
|
+
<button @click="showAvatar = !showAvatar">Toggle avatar slot</button>
|
|
116
|
+
<button @click="short = !short">Resize height</button>
|
|
117
|
+
<button @click="showQuestions = !showQuestions">Toggle questions</button>
|
|
118
|
+
<button @click="attachments.status.value = 'An upload failed. Retry or remove it.'">Show attachment error</button>
|
|
119
|
+
<output>Avatar requested: {{ avatarSize }}</output>
|
|
120
|
+
</template>
|
|
99
121
|
<output>{{ phase }}; submitted {{ submissions }}</output>
|
|
100
122
|
<output v-if="capabilitiesEnabled">Model: {{ appliedModel }}; sent files: {{ attachmentReceipts.map(item => item.fileName).join(', ') }}</output>
|
|
101
123
|
</div>
|
|
102
|
-
<main class="fixture" :class="{ narrow }">
|
|
103
|
-
<AssistantConversationElement :adapter="adapter">
|
|
124
|
+
<main class="fixture" :class="{ narrow, 'fixture--avatar': avatarFixture, 'fixture--short': short }">
|
|
125
|
+
<AssistantConversationElement v-model:avatar-size="avatarSize" :adapter="adapter">
|
|
126
|
+
<template v-if="showAvatar" #avatar><svg viewBox="0 0 100 100" role="img" aria-label="Support assistant avatar"><circle cx="50" cy="50" r="45" fill="#6750a4" /><path d="M30 62Q50 80 70 62M35 40h1m28 0h1" fill="none" stroke="white" stroke-width="6" stroke-linecap="round" /></svg></template>
|
|
104
127
|
<template v-if="customActivity" #activity="{ activity }"><strong>Custom activity: {{ activity.label }}</strong></template>
|
|
105
128
|
<template #composer-feedback><p v-if="feedback" class="feedback" role="status">Describe what you want to change.</p></template>
|
|
106
129
|
</AssistantConversationElement>
|
|
@@ -113,5 +136,7 @@ const adapter = reactive({
|
|
|
113
136
|
.controls button { border: 1px solid; padding: 4px; }
|
|
114
137
|
.fixture { height: 70vh; width: min(900px, 100%); margin: auto; }
|
|
115
138
|
.fixture.narrow { width: min(320px, 100%); }
|
|
139
|
+
.fixture--avatar { width: min(320px, 100%); }
|
|
140
|
+
.fixture--short { height: 420px; }
|
|
116
141
|
.feedback { flex: 1 1 24rem; min-width: 0; margin: 0; }
|
|
117
142
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.203",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
46
46
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
47
47
|
"@ai-sdk/xai": "^4.0.58",
|
|
48
|
-
"@jskit-ai/resource-core": "0.1.
|
|
49
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
48
|
+
"@jskit-ai/resource-core": "0.1.167",
|
|
49
|
+
"@jskit-ai/resource-crud-core": "0.1.167",
|
|
50
50
|
"@mdi/js": "^7.4.47",
|
|
51
51
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
52
52
|
"ai": "^7.0.101",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"ws": "^8.20.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
59
|
-
"@jskit-ai/kernel": "0.1.
|
|
58
|
+
"@jskit-ai/http-runtime": "0.1.223",
|
|
59
|
+
"@jskit-ai/kernel": "0.1.225",
|
|
60
60
|
"vue": "^3.5.13",
|
|
61
61
|
"vuetify": "^4.0.0"
|
|
62
62
|
},
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="assistant-conversation" :aria-label="label">
|
|
2
|
+
<section ref="container" class="assistant-conversation" :aria-label="label">
|
|
3
|
+
<section v-if="$slots.avatar" class="assistant-conversation__avatar" aria-label="Conversation avatar">
|
|
4
|
+
<div v-if="$slots.composer || !adapter.composer" ref="avatarControls" class="assistant-conversation__avatar-controls">
|
|
5
|
+
<v-btn
|
|
6
|
+
ref="avatarControl" class="assistant-conversation__avatar-size" aria-label="Avatar size" :title="`Avatar size: ${avatarSize}`"
|
|
7
|
+
aria-haspopup="menu" :aria-expanded="avatarMenuOpen" :icon="mdiAccountCircleOutline" size="small" variant="text"
|
|
8
|
+
/>
|
|
9
|
+
</div>
|
|
10
|
+
<v-menu v-model="avatarMenuOpen" :activator="avatarControl?.$el">
|
|
11
|
+
<v-list role="menu" aria-label="Avatar size">
|
|
12
|
+
<v-list-item
|
|
13
|
+
v-for="size in avatarSizes" :key="size.value" role="menuitem" :title="size.title"
|
|
14
|
+
:active="size.value === avatarSize" @click="$emit('update:avatarSize', size.value)"
|
|
15
|
+
/>
|
|
16
|
+
</v-list>
|
|
17
|
+
</v-menu>
|
|
18
|
+
<div v-if="avatarSize !== 'hidden'" class="assistant-conversation__avatar-visual" :style="{ height: `${avatarHeight}px` }">
|
|
19
|
+
<slot name="avatar" :size="avatarSize" :height="avatarHeight" />
|
|
20
|
+
</div>
|
|
21
|
+
</section>
|
|
3
22
|
<AssistantTranscript
|
|
4
23
|
v-bind="conversation" :working="working" class="assistant-conversation__transcript"
|
|
5
24
|
@load-more="adapter.actions?.loadMore?.($event)" @reload="adapter.actions?.reload?.()"
|
|
@@ -80,6 +99,10 @@
|
|
|
80
99
|
@update:model-value="updateConfiguration({ ...configuration, [field.name]: $event })"
|
|
81
100
|
/>
|
|
82
101
|
</slot>
|
|
102
|
+
<v-btn
|
|
103
|
+
v-if="$slots.avatar" ref="avatarControl" class="assistant-conversation__avatar-size" aria-label="Avatar size" :title="`Avatar size: ${avatarSize}`"
|
|
104
|
+
aria-haspopup="menu" :aria-expanded="avatarMenuOpen" :icon="mdiAccountCircleOutline" size="small" variant="text"
|
|
105
|
+
/>
|
|
83
106
|
<slot name="composer-tools" :adapter="adapter" />
|
|
84
107
|
<template #feedback>
|
|
85
108
|
<p v-if="attachmentsEnabled && adapter.attachments.status" role="alert" class="assistant-conversation__attachment-error">{{ adapter.attachments.status }}</p>
|
|
@@ -92,8 +115,8 @@
|
|
|
92
115
|
</section>
|
|
93
116
|
</template>
|
|
94
117
|
<script setup>
|
|
95
|
-
import { computed, ref, useId, useSlots, watch } from "vue";
|
|
96
|
-
import { mdiPaperclip } from "@mdi/js";
|
|
118
|
+
import { computed, onBeforeUnmount, onMounted, onUpdated, ref, useId, useSlots, watch } from "vue";
|
|
119
|
+
import { mdiAccountCircleOutline, mdiPaperclip } from "@mdi/js";
|
|
97
120
|
import AssistantTranscript from "./AssistantTranscript.vue";
|
|
98
121
|
import AssistantPromptInput from "./AssistantPromptInput.vue";
|
|
99
122
|
import AssistantComposerActions from "./AssistantComposerActions.vue";
|
|
@@ -104,12 +127,69 @@ import AssistantQuestionInputs from "./AssistantQuestionInputs.vue";
|
|
|
104
127
|
import AssistantModelControl from "./AssistantModelControl.vue";
|
|
105
128
|
const props = defineProps({
|
|
106
129
|
adapter: { type: Object, required: true },
|
|
130
|
+
avatarSize: { type: String, default: "compact", validator: (value) => ["hidden", "compact", "standard", "large"].includes(value) },
|
|
107
131
|
label: { type: String, default: "Assistant conversation" },
|
|
108
132
|
configuration: { type: Object, default: () => ({}) },
|
|
109
133
|
configurationFields: { type: Array, default: () => [] },
|
|
110
134
|
configurationMode: { type: String, default: "hidden", validator: (value) => ["hidden", "readonly", "editable"].includes(value) }
|
|
111
135
|
});
|
|
136
|
+
defineEmits(["update:avatarSize"]);
|
|
112
137
|
const slots = useSlots();
|
|
138
|
+
const container = ref(null);
|
|
139
|
+
const avatarControls = ref(null);
|
|
140
|
+
const avatarControl = ref(null);
|
|
141
|
+
const avatarMenuOpen = ref(false);
|
|
142
|
+
const avatarSizes = [
|
|
143
|
+
{ title: "Hidden", value: "hidden", height: 0 },
|
|
144
|
+
{ title: "Compact", value: "compact", height: 64 },
|
|
145
|
+
{ title: "Standard", value: "standard", height: 112 },
|
|
146
|
+
{ title: "Large", value: "large", height: 176 }
|
|
147
|
+
];
|
|
148
|
+
const avatarSpace = ref(0);
|
|
149
|
+
const avatarHeight = computed(() => Math.min(avatarSpace.value, avatarSizes.find(size => size.value === props.avatarSize).height));
|
|
150
|
+
let avatarObserver;
|
|
151
|
+
const observedAvatarElements = new Set();
|
|
152
|
+
function measureAvatarSpace() {
|
|
153
|
+
const root = container.value;
|
|
154
|
+
if (!root || !slots.avatar) return;
|
|
155
|
+
const rootStyle = getComputedStyle(root);
|
|
156
|
+
let occupied = parseFloat(rootStyle.paddingTop) + parseFloat(rootStyle.paddingBottom);
|
|
157
|
+
for (const element of observedAvatarElements) {
|
|
158
|
+
if (element === root) continue;
|
|
159
|
+
const style = getComputedStyle(element);
|
|
160
|
+
occupied += element.getBoundingClientRect().height + parseFloat(style.marginTop) + parseFloat(style.marginBottom);
|
|
161
|
+
}
|
|
162
|
+
// Shrink artwork before the composer, feedback, or the readable transcript.
|
|
163
|
+
avatarSpace.value = Math.max(0, root.clientHeight - occupied - 120);
|
|
164
|
+
}
|
|
165
|
+
function observeAvatarSpace() {
|
|
166
|
+
if (!container.value || !slots.avatar) {
|
|
167
|
+
avatarObserver?.disconnect();
|
|
168
|
+
observedAvatarElements.clear();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
avatarObserver ||= new ResizeObserver(measureAvatarSpace);
|
|
172
|
+
const fixedElements = Array.from(container.value.children).filter(element =>
|
|
173
|
+
!element.classList.contains("assistant-conversation__avatar") && !element.classList.contains("assistant-conversation__transcript"));
|
|
174
|
+
const elements = new Set([container.value, ...fixedElements]);
|
|
175
|
+
if (avatarControls.value) elements.add(avatarControls.value);
|
|
176
|
+
for (const element of observedAvatarElements) {
|
|
177
|
+
if (!elements.has(element)) {
|
|
178
|
+
avatarObserver.unobserve(element);
|
|
179
|
+
observedAvatarElements.delete(element);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const element of elements) {
|
|
183
|
+
if (!observedAvatarElements.has(element)) {
|
|
184
|
+
avatarObserver.observe(element);
|
|
185
|
+
observedAvatarElements.add(element);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
measureAvatarSpace();
|
|
189
|
+
}
|
|
190
|
+
onMounted(observeAvatarSpace);
|
|
191
|
+
onUpdated(observeAvatarSpace);
|
|
192
|
+
onBeforeUnmount(() => avatarObserver?.disconnect());
|
|
113
193
|
const transcriptSlots = computed(() => ["welcome", "attachments", "system-message", "message-actions"].filter((name) => slots[name]));
|
|
114
194
|
const input = ref(null);
|
|
115
195
|
const fileInput = ref(null);
|
|
@@ -248,6 +328,11 @@ defineExpose({ focus: () => input.value?.focus(), submit, stop });
|
|
|
248
328
|
</script>
|
|
249
329
|
<style scoped>
|
|
250
330
|
.assistant-conversation { display: flex; flex-direction: column; min-height: 0; min-width: 0; height: 100%; gap: 0; container: assistant-conversation / inline-size; }
|
|
331
|
+
.assistant-conversation__avatar { flex: 0 0 auto; min-width: 0; }
|
|
332
|
+
.assistant-conversation__avatar-controls { display: flex; align-items: center; justify-content: flex-end; min-height: 48px; }
|
|
333
|
+
.assistant-conversation__avatar-size { min-width: 48px; min-height: 48px; }
|
|
334
|
+
.assistant-conversation__avatar-visual { display: flex; justify-content: center; min-height: 0; overflow: hidden; }
|
|
335
|
+
.assistant-conversation__avatar-visual :deep(> *) { max-width: 100%; max-height: 100%; }
|
|
251
336
|
.assistant-conversation__goal { display: flex; justify-content: flex-end; flex: 0 0 auto; }
|
|
252
337
|
.assistant-conversation__transcript { flex: 1 1 auto; min-height: 0; }
|
|
253
338
|
.assistant-conversation__composer { flex: 0 0 auto; }
|
|
@@ -50,6 +50,10 @@ export function createClaudeConversationDriver({ connections, host = {}, limits
|
|
|
50
50
|
|
|
51
51
|
async function captureAccountStatus({ command, args, cwd, baseEnv, signal, timeout, maxBuffer }) {
|
|
52
52
|
signal.throwIfAborted();
|
|
53
|
+
if (typeof host.execution?.run === "function") {
|
|
54
|
+
return host.execution.run({ command, args, cwd, baseEnv, signal, timeout, maxBuffer,
|
|
55
|
+
mode: "capture", limits: host.limits });
|
|
56
|
+
}
|
|
53
57
|
const native = await execution.start({ command, args, cwd, env: baseEnv, stream: true, limits: host.limits });
|
|
54
58
|
const cancelled = Promise.withResolvers();
|
|
55
59
|
const abort = () => cancelled.reject(signal.reason);
|
|
@@ -522,9 +522,11 @@ test("optional goals show running and paused lights, count only active time, and
|
|
|
522
522
|
try {
|
|
523
523
|
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
|
|
524
524
|
await page.clock.install();
|
|
525
|
+
await page.clock.pauseAt(new Date());
|
|
525
526
|
await page.goto(`${vite.baseURL}/?conversation=1&goal=1`);
|
|
526
527
|
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
527
528
|
await page.getByRole("button", { name: "Set goal", exact: true }).click();
|
|
529
|
+
await page.clock.runFor(250);
|
|
528
530
|
await page.getByRole("textbox", { name: "Goal objective" }).fill("Complete the shared assistant.");
|
|
529
531
|
await page.getByRole("spinbutton", { name: "Token budget (optional)" }).fill("10000");
|
|
530
532
|
await page.getByRole("button", { name: "Start goal", exact: true }).click();
|
|
@@ -541,6 +543,7 @@ test("optional goals show running and paused lights, count only active time, and
|
|
|
541
543
|
await expect(input).toBeFocused();
|
|
542
544
|
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
|
|
543
545
|
await page.getByRole("button", { name: "Goal running", exact: true }).click();
|
|
546
|
+
await page.clock.runFor(250);
|
|
544
547
|
await page.getByRole("button", { name: "Pause goal", exact: true }).click();
|
|
545
548
|
await expect(light).toHaveCSS("background-color", "rgb(239, 108, 0)");
|
|
546
549
|
await expect(light).toHaveCSS("animation-name", "none");
|
|
@@ -558,6 +561,7 @@ test("optional goals show running and paused lights, count only active time, and
|
|
|
558
561
|
await expect(elapsed).toBeHidden();
|
|
559
562
|
await expect(light).toBeVisible();
|
|
560
563
|
await page.getByRole("button", { name: "Goal running", exact: true }).click();
|
|
564
|
+
await page.clock.runFor(250);
|
|
561
565
|
await expect(page.getByText("Running time: 1:10", { exact: true })).toBeVisible();
|
|
562
566
|
await page.getByRole("button", { name: "Toggle goals" }).evaluate(element => element.click());
|
|
563
567
|
await expect(page.locator(".assistant-goal")).toHaveCount(0);
|
|
@@ -617,3 +621,103 @@ test("shared model choices and file delivery preserve a responsive composer", {
|
|
|
617
621
|
await stopProcess(vite);
|
|
618
622
|
}
|
|
619
623
|
});
|
|
624
|
+
|
|
625
|
+
test("an optional avatar clamps in a short conversation without consuming its preference or draft", {
|
|
626
|
+
skip: !RUN_BROWSER_TEST, timeout: 90_000
|
|
627
|
+
}, async () => {
|
|
628
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
629
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
630
|
+
try {
|
|
631
|
+
for (const width of [390, 800, 1365]) {
|
|
632
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
633
|
+
await page.goto(`${vite.baseURL}/?conversation=1&avatar=1&capabilities=1`);
|
|
634
|
+
const panel = page.locator(".fixture");
|
|
635
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
636
|
+
const body = page.locator(".assistant-transcript__body");
|
|
637
|
+
const artwork = page.locator(".assistant-conversation__avatar-visual");
|
|
638
|
+
const sizeControl = page.getByRole("button", { name: "Avatar size", exact: true });
|
|
639
|
+
const selectSize = async name => {
|
|
640
|
+
await sizeControl.focus();
|
|
641
|
+
await sizeControl.press("Enter");
|
|
642
|
+
await page.getByRole("menuitem", { name, exact: true }).click();
|
|
643
|
+
await expect(page.getByRole("menu", { name: "Avatar size", exact: true })).toBeHidden();
|
|
644
|
+
await expect(page.getByText(`Avatar requested: ${name.toLowerCase()}`, { exact: true })).toBeVisible();
|
|
645
|
+
};
|
|
646
|
+
const assertComposerFits = async () => {
|
|
647
|
+
const bounds = await panel.boundingBox();
|
|
648
|
+
const send = await page.getByRole("button", { name: "Send", exact: true }).boundingBox();
|
|
649
|
+
const feedback = await page.getByText("Describe what you want to change.", { exact: true }).boundingBox();
|
|
650
|
+
const avatarButton = await sizeControl.boundingBox();
|
|
651
|
+
assert.ok(send.y + send.height <= bounds.y + bounds.height + 1, "Send stays inside the supplied container");
|
|
652
|
+
assert.ok(feedback.y + feedback.height <= bounds.y + bounds.height + 1, "Feedback stays inside the supplied container");
|
|
653
|
+
assert.ok((await input.boundingBox()).height >= 40);
|
|
654
|
+
assert.ok(avatarButton.width >= 48 && avatarButton.height >= 48, "Avatar sizing retains a reachable touch target");
|
|
655
|
+
await expect.poll(async () => (await body.boundingBox()).height, { message: "The short container retains readable history" }).toBeGreaterThanOrEqual(100);
|
|
656
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
657
|
+
};
|
|
658
|
+
assert.equal(Math.round((await panel.boundingBox()).width), 320);
|
|
659
|
+
assert.equal(Math.round((await panel.boundingBox()).height), 420);
|
|
660
|
+
await expect(artwork).toHaveCSS("height", "64px");
|
|
661
|
+
await expect(page.locator('[data-message-role="assistant"]')).toHaveCount(20);
|
|
662
|
+
await page.evaluate(() => {
|
|
663
|
+
window.avatarElements = [document.querySelector(".assistant-conversation"), document.querySelector("textarea"), document.querySelector(".assistant-transcript__body")];
|
|
664
|
+
});
|
|
665
|
+
await input.fill("Keep my typed support question.");
|
|
666
|
+
await input.evaluate(element => element.setSelectionRange(5, 9));
|
|
667
|
+
await body.evaluate(element => { element.scrollTop = 100; });
|
|
668
|
+
const before = await body.evaluate(element => element.scrollTop);
|
|
669
|
+
await selectSize("Large");
|
|
670
|
+
await expect.poll(async () => (await artwork.boundingBox()).height).toBeLessThan(176);
|
|
671
|
+
await assertComposerFits();
|
|
672
|
+
assert.equal(await body.evaluate(element => element.scrollTop), before, "Avatar resizing leaves a scrolled-up reader in place");
|
|
673
|
+
await selectSize("Hidden");
|
|
674
|
+
await expect(artwork).toHaveCount(0);
|
|
675
|
+
await expect(sizeControl).toBeVisible();
|
|
676
|
+
await selectSize("Standard");
|
|
677
|
+
await expect.poll(async () => (await artwork.boundingBox()).height).toBeGreaterThan(0);
|
|
678
|
+
await selectSize("Large");
|
|
679
|
+
await input.focus();
|
|
680
|
+
await input.evaluate(element => element.setSelectionRange(5, 9));
|
|
681
|
+
await expect(input).toBeFocused();
|
|
682
|
+
const resize = () => page.getByRole("button", { name: "Resize height", exact: true }).evaluate(element => element.click());
|
|
683
|
+
await resize();
|
|
684
|
+
await expect(artwork).toHaveCSS("height", "176px");
|
|
685
|
+
await resize();
|
|
686
|
+
await expect.poll(async () => (await artwork.boundingBox()).height).toBeLessThan(176);
|
|
687
|
+
await expect(input).toBeFocused();
|
|
688
|
+
await expect(input).toHaveValue("Keep my typed support question.");
|
|
689
|
+
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
|
|
690
|
+
await expect(page.getByText("Avatar requested: large", { exact: true })).toBeVisible();
|
|
691
|
+
assert.equal(await body.evaluate(element => element.scrollTop), before);
|
|
692
|
+
await page.getByRole("button", { name: "Toggle questions", exact: true }).evaluate(element => element.click());
|
|
693
|
+
const answer = page.getByRole("textbox", { name: "[1] What should happen next?", exact: true });
|
|
694
|
+
await expect(answer).toBeVisible();
|
|
695
|
+
await answer.fill("Recover my account.");
|
|
696
|
+
await assertComposerFits();
|
|
697
|
+
await page.getByRole("button", { name: "Answer normally instead", exact: true }).click();
|
|
698
|
+
await expect(answer).toHaveCount(0);
|
|
699
|
+
await input.fill("Keep my typed support question.");
|
|
700
|
+
await page.locator('input[type="file"]').setInputFiles({ name: "account.txt", mimeType: "text/plain", buffer: Buffer.from("Account recovery notes") });
|
|
701
|
+
await page.getByRole("button", { name: "Finish uploads", exact: true }).click();
|
|
702
|
+
await expect(page.getByText("1 of 1 ready", { exact: true })).toBeVisible();
|
|
703
|
+
await page.getByRole("button", { name: "Show attachment error", exact: true }).click();
|
|
704
|
+
await expect(page.getByText("An upload failed. Retry or remove it.", { exact: true })).toBeVisible();
|
|
705
|
+
await assertComposerFits();
|
|
706
|
+
await expect(input).toHaveValue("Keep my typed support question.");
|
|
707
|
+
await page.getByRole("button", { name: "Remove account.txt", exact: true }).click();
|
|
708
|
+
await page.getByRole("button", { name: "Toggle avatar slot", exact: true }).evaluate(element => element.click());
|
|
709
|
+
await expect(sizeControl).toHaveCount(0);
|
|
710
|
+
await expect(page.getByRole("region", { name: "Conversation avatar", exact: true })).toHaveCount(0);
|
|
711
|
+
await page.getByRole("button", { name: "Toggle avatar slot", exact: true }).evaluate(element => element.click());
|
|
712
|
+
await expect(sizeControl).toBeVisible();
|
|
713
|
+
await expect(page.getByText("Avatar requested: large", { exact: true })).toBeVisible();
|
|
714
|
+
assert.equal(await page.evaluate(() => window.avatarElements.every(element => element.isConnected)), true, "Sizing and slot changes retain the same conversation, input and transcript nodes");
|
|
715
|
+
await expect(page.locator('[data-message-role="assistant"]')).toHaveCount(20);
|
|
716
|
+
await expect(page.getByText("idle; submitted 0", { exact: true })).toBeVisible();
|
|
717
|
+
await page.close();
|
|
718
|
+
}
|
|
719
|
+
} finally {
|
|
720
|
+
await browser.close();
|
|
721
|
+
await stopProcess(vite);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
@@ -12,6 +12,34 @@ import { createSchema } from "json-rest-schema";
|
|
|
12
12
|
const configuration = { systemPrompt: "Keep these instructions fresh.", model: "test-model", effort: "high" };
|
|
13
13
|
const input = { messageId: "first", text: "Hello" };
|
|
14
14
|
|
|
15
|
+
test("Claude account checks use the supplied finite capture policy without starting a conversation service", async t => {
|
|
16
|
+
const local = createLocalConversationExecution();
|
|
17
|
+
const checks = [];
|
|
18
|
+
const f = await fixture(t, { execution: {
|
|
19
|
+
async run(request) {
|
|
20
|
+
checks.push(request);
|
|
21
|
+
assert.equal(request.mode, "capture");
|
|
22
|
+
assert.equal(request.cwd, request.baseEnv.HOME);
|
|
23
|
+
assert.deepEqual(request.args, ["auth", "status", "--json"]);
|
|
24
|
+
assert.equal(request.timeout, 30_000);
|
|
25
|
+
assert.equal(request.maxBuffer, 64 * 1024);
|
|
26
|
+
assert.equal(request.signal.aborted, false);
|
|
27
|
+
return { ok: true, exitCode: 0, stdout: JSON.stringify({ loggedIn: true,
|
|
28
|
+
authMethod: "claude.ai", email: "owner@example.test" }) };
|
|
29
|
+
},
|
|
30
|
+
start(request) {
|
|
31
|
+
assert.equal(request.args.includes("auth"), false, "Account checks must not borrow the native-turn execution policy");
|
|
32
|
+
return local.start(request);
|
|
33
|
+
},
|
|
34
|
+
stop: local.stop
|
|
35
|
+
} });
|
|
36
|
+
await f.conversation.send(input);
|
|
37
|
+
const result = await f.conversation.wait();
|
|
38
|
+
assert.equal(result.conversationLog[0].metadata.runtime.status, "complete", result.error);
|
|
39
|
+
assert.equal(result.conversationLog[0].assistant.text, "Answer: Hello");
|
|
40
|
+
assert.ok(checks.length > 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
15
43
|
test("Claude exposes the durable native identity before dispatch and respects a rejected gate", async t => {
|
|
16
44
|
const f = await fixture(t);
|
|
17
45
|
await f.first.close();
|