@jskit-ai/assistant-core 0.1.168 → 0.1.170
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 +3 -3
- package/fixtures/responsive-assistant/App.vue +30 -92
- package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
- package/package.json +5 -7
- package/src/client/conversation/AssistantComposerActions.vue +4 -2
- package/src/client/conversation/AssistantConversationElement.vue +4 -2
- package/src/client/conversation/AssistantTranscript.vue +12 -7
- package/src/client/index.js +0 -2
- package/src/server/conversation/codexClient.js +1 -0
- package/test/assistantScroll.browser.test.js +65 -82
- package/test/codexClient.test.js +9 -0
- package/test/componentContracts.test.js +10 -23
- package/src/client/components/AssistantClientElement.vue +0 -1327
- package/src/client/lib/markdownRenderer.js +0 -31
|
@@ -8,9 +8,9 @@
|
|
|
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.170",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.190",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.192",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0"
|
|
@@ -1,106 +1,44 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, ref } from "vue";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
import AssistantConversationElement from "../../src/client/conversation/AssistantConversationElement.vue";
|
|
4
|
+
import { conversationTurnsFromMessages } from "../../src/shared/conversation/turns.js";
|
|
5
|
+
const draft = ref("");
|
|
6
|
+
const controls = new URLSearchParams(location.search).has("controls");
|
|
7
|
+
const scope = ref("one");
|
|
8
|
+
const messages = ref(Array.from({ length: 70 }, (_, index) => ({
|
|
9
|
+
id: `message-${index}`, role: index % 2 ? "assistant" : "user", status: "completed",
|
|
10
|
+
text: `Conversation line ${index + 1}: responsive scroll containment keeps the composer visible.\n\nMore text with **formatting**, a [link](https://example.com), and a_long_word_that_must_wrap_in_narrow_panes_without_overflow.`
|
|
11
|
+
})));
|
|
12
|
+
if (controls) messages.value[0].text = messages.value[0].text.repeat(4);
|
|
13
|
+
const adapter = computed(() => ({
|
|
14
|
+
conversation: { turns: conversationTurnsFromMessages(messages.value), scrollKey: scope.value, hasMoreBefore: controls },
|
|
15
|
+
composer: { draft: draft.value, canSend: Boolean(draft.value.trim()), rows: 2 },
|
|
16
|
+
actions: {
|
|
17
|
+
setDraft(value) { draft.value = value; },
|
|
18
|
+
submit() { draft.value = ""; },
|
|
19
|
+
loadMore({ complete }) {
|
|
20
|
+
messages.value.unshift({ id: "older", role: "assistant", text: "Older conversation paragraph. ".repeat(40), status: "completed" });
|
|
21
|
+
complete({ changed: true });
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
|
-
|
|
19
|
-
const input = ref("");
|
|
20
|
-
const isStreaming = ref(false);
|
|
21
|
-
const isRestoringConversation = ref(false);
|
|
22
|
-
const conversationId = ref("conversation-1");
|
|
23
|
-
const conversationHistory = ref([
|
|
24
|
-
{
|
|
25
|
-
id: "conversation-1",
|
|
26
|
-
title: "Responsive layout regression",
|
|
27
|
-
status: "completed",
|
|
28
|
-
startedAt: "2026-08-24T00:00:00.000Z",
|
|
29
|
-
messageCount: 1
|
|
30
|
-
}
|
|
31
|
-
]);
|
|
32
|
-
const conversationHistoryLoading = ref(false);
|
|
33
|
-
const conversationHistoryLoadingMore = ref(false);
|
|
34
|
-
const conversationHistoryHasMore = ref(false);
|
|
35
|
-
const conversationHistoryError = ref("");
|
|
36
|
-
const pendingToolEvents = ref([
|
|
37
|
-
{
|
|
38
|
-
id: "tool-1",
|
|
39
|
-
name: "assistant_action_search",
|
|
40
|
-
status: "completed"
|
|
41
|
-
}
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
const state = Object.freeze({
|
|
45
|
-
messages,
|
|
46
|
-
input,
|
|
47
|
-
isStreaming,
|
|
48
|
-
isRestoringConversation,
|
|
49
|
-
pendingToolEvents,
|
|
50
|
-
conversationId,
|
|
51
|
-
conversationHistory,
|
|
52
|
-
conversationHistoryLoading,
|
|
53
|
-
conversationHistoryLoadingMore,
|
|
54
|
-
conversationHistoryHasMore,
|
|
55
|
-
conversationHistoryError,
|
|
56
|
-
isAdminSurface: ref(false),
|
|
57
|
-
canSend: computed(() => input.value.trim().length > 0),
|
|
58
|
-
canStartNewConversation: ref(true)
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const actions = Object.freeze({
|
|
62
|
-
async sendMessage() {},
|
|
63
|
-
handleInputKeydown() {},
|
|
64
|
-
async cancelStream() {},
|
|
65
|
-
async startNewConversation() {},
|
|
66
|
-
async selectConversation() {},
|
|
67
|
-
async refreshConversationHistory() {},
|
|
68
|
-
async loadMoreConversationHistory() {}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const meta = Object.freeze({
|
|
72
|
-
formatConversationStartedAt() {
|
|
73
|
-
return "Aug 24, 2026";
|
|
74
|
-
}
|
|
75
|
-
});
|
|
24
|
+
}));
|
|
76
25
|
</script>
|
|
77
|
-
|
|
78
26
|
<template>
|
|
79
27
|
<v-app>
|
|
80
28
|
<v-main>
|
|
81
29
|
<main class="assistant-responsive-fixture">
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/>
|
|
30
|
+
<nav v-if="controls">
|
|
31
|
+
<button @click="scope = scope === 'one' ? 'two' : 'one'">Change conversation</button>
|
|
32
|
+
<button @click="messages.push({ id: `added-${messages.length}`, role: 'assistant', text: 'New reply. '.repeat(30), status: 'completed' })">Append reply</button>
|
|
33
|
+
</nav>
|
|
34
|
+
<AssistantConversationElement :adapter="adapter" class="assistant-responsive-fixture__conversation" />
|
|
88
35
|
</main>
|
|
89
36
|
</v-main>
|
|
90
37
|
</v-app>
|
|
91
38
|
</template>
|
|
92
|
-
|
|
93
39
|
<style>
|
|
94
|
-
html,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
margin: 0;
|
|
99
|
-
min-block-size: 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.assistant-responsive-fixture {
|
|
103
|
-
block-size: 100%;
|
|
104
|
-
min-block-size: 0;
|
|
105
|
-
}
|
|
40
|
+
html, body, #app { height: 100%; margin: 0; min-height: 0; }
|
|
41
|
+
.assistant-responsive-fixture { display: flex; flex-direction: column; height: 100dvh; min-height: 0; padding: .5rem; }
|
|
42
|
+
.assistant-responsive-fixture nav { display: flex; flex: 0 0 auto; gap: 1rem; }
|
|
43
|
+
.assistant-responsive-fixture__conversation { flex: 1 1 auto; }
|
|
106
44
|
</style>
|
|
@@ -5,11 +5,13 @@ const phase = ref("idle");
|
|
|
5
5
|
const draft = ref("");
|
|
6
6
|
const narrow = ref(false);
|
|
7
7
|
const submissions = ref(0);
|
|
8
|
+
const feedback = ref(false);
|
|
8
9
|
const adapter = reactive({
|
|
9
10
|
conversation: { turns: [], visible: true, scrollKey: "fixture", welcomeMessage: "Composer responsiveness fixture" },
|
|
10
11
|
composer: {
|
|
11
12
|
draft,
|
|
12
13
|
rows: 2,
|
|
14
|
+
density: computed(() => narrow.value ? "compact" : "default"),
|
|
13
15
|
submitOnEnter: true,
|
|
14
16
|
canSend: computed(() => Boolean(draft.value.trim()) && ["idle", "active", "stopped"].includes(phase.value)),
|
|
15
17
|
canStop: computed(() => ["active", "stopping"].includes(phase.value)),
|
|
@@ -29,10 +31,13 @@ const adapter = reactive({
|
|
|
29
31
|
<div class="controls">
|
|
30
32
|
<button v-for="state in ['idle', 'active', 'reconnecting', 'stopping', 'stopped']" :key="state" @click="phase = state">External {{ state }}</button>
|
|
31
33
|
<button @click="narrow = !narrow">Resize pane</button>
|
|
34
|
+
<button @click="feedback = !feedback">Toggle action feedback</button>
|
|
32
35
|
<output>{{ phase }}; submitted {{ submissions }}</output>
|
|
33
36
|
</div>
|
|
34
37
|
<main class="fixture" :class="{ narrow }">
|
|
35
|
-
<AssistantConversationElement :adapter="adapter"
|
|
38
|
+
<AssistantConversationElement :adapter="adapter">
|
|
39
|
+
<template #composer-feedback><p v-if="feedback" class="feedback" role="status">Describe what you want to change.</p></template>
|
|
40
|
+
</AssistantConversationElement>
|
|
36
41
|
</main>
|
|
37
42
|
</v-main>
|
|
38
43
|
</v-app>
|
|
@@ -42,4 +47,5 @@ const adapter = reactive({
|
|
|
42
47
|
.controls button { border: 1px solid; padding: 4px; }
|
|
43
48
|
.fixture { height: 70vh; width: min(900px, 100%); margin: auto; }
|
|
44
49
|
.fixture.narrow { width: min(320px, 100%); }
|
|
50
|
+
.feedback { flex: 1 1 24rem; min-width: 0; margin: 0; }
|
|
45
51
|
</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.170",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -21,18 +21,16 @@
|
|
|
21
21
|
"./testing/conversation-storage": "./src/testing/conversationStorageContract.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@jskit-ai/resource-core": "0.1.
|
|
25
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
26
|
-
"dompurify": "^3.4.13",
|
|
24
|
+
"@jskit-ai/resource-core": "0.1.134",
|
|
25
|
+
"@jskit-ai/resource-crud-core": "0.1.134",
|
|
27
26
|
"json-rest-schema": "^1.0.17",
|
|
28
|
-
"marked": "^17.0.4",
|
|
29
27
|
"openai": "^6.22.0",
|
|
30
28
|
"@mdi/js": "^7.4.47",
|
|
31
29
|
"ws": "^8.20.1"
|
|
32
30
|
},
|
|
33
31
|
"peerDependencies": {
|
|
34
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
35
|
-
"@jskit-ai/kernel": "0.1.
|
|
32
|
+
"@jskit-ai/http-runtime": "0.1.190",
|
|
33
|
+
"@jskit-ai/kernel": "0.1.192",
|
|
36
34
|
"vue": "^3.5.13",
|
|
37
35
|
"vuetify": "^4.0.0"
|
|
38
36
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="assistant-composer-actions">
|
|
2
|
+
<div class="assistant-composer-actions" :class="{ 'assistant-composer-actions--compact': state.density === 'compact' }">
|
|
3
3
|
<div class="assistant-composer-actions__tools"><slot /></div>
|
|
4
4
|
<div class="assistant-composer-actions__delivery">
|
|
5
5
|
<v-btn
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
{{ state.submitLabel || (state.pending ? 'Sending…' : 'Send') }}
|
|
19
19
|
</v-btn>
|
|
20
20
|
</div>
|
|
21
|
+
<slot name="feedback" />
|
|
21
22
|
</div>
|
|
22
23
|
</template>
|
|
23
24
|
<script setup>
|
|
@@ -36,7 +37,8 @@ defineExpose({ focus() { (sendButton.value?.$el || sendButton.value)?.focus?.();
|
|
|
36
37
|
.assistant-composer-actions__tools { flex: 1 1 auto; flex-wrap: wrap; }
|
|
37
38
|
.assistant-composer-actions__delivery { flex: 0 0 auto; margin-left: auto; }
|
|
38
39
|
.assistant-composer-actions__send { min-width: 5.25rem; }
|
|
39
|
-
|
|
40
|
+
.assistant-composer-actions--compact .assistant-composer-actions__delivery > .v-btn { min-height: 3rem; }
|
|
41
|
+
@media (max-width: 600px), (pointer: coarse) {
|
|
40
42
|
.assistant-composer-actions__delivery > .v-btn { min-height: 3rem; min-width: 3rem; }
|
|
41
43
|
.assistant-composer-actions__delivery > .assistant-composer-actions__send { min-width: 5.25rem; }
|
|
42
44
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<slot name="hints" :adapter="adapter" />
|
|
12
12
|
<slot name="composer" :adapter="adapter">
|
|
13
13
|
<AssistantPromptInput
|
|
14
|
-
v-if="adapter.composer" ref="input" v-bind="adapter.composer"
|
|
14
|
+
v-if="adapter.composer" ref="input" v-bind="adapter.composer" class="assistant-conversation__composer"
|
|
15
15
|
:model-value="adapter.composer.draft || ''" :submit-enabled="adapter.composer.canSend"
|
|
16
16
|
:aria-label="adapter.composer.ariaLabel || 'Message AI assistant'" :rows="adapter.composer.rows || 2"
|
|
17
17
|
tab-to-submit @update:model-value="adapter.actions.setDraft($event)"
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
/>
|
|
35
35
|
</slot>
|
|
36
36
|
<slot name="composer-tools" :adapter="adapter" />
|
|
37
|
+
<template #feedback><slot name="composer-feedback" :adapter="adapter" /></template>
|
|
37
38
|
</AssistantComposerActions>
|
|
38
39
|
</template>
|
|
39
40
|
</AssistantPromptInput>
|
|
@@ -53,7 +54,7 @@ const props = defineProps({
|
|
|
53
54
|
configurationMode: { type: String, default: "hidden", validator: (value) => ["hidden", "readonly", "editable"].includes(value) }
|
|
54
55
|
});
|
|
55
56
|
const slots = useSlots();
|
|
56
|
-
const transcriptSlots = computed(() => ["attachments", "system-message", "message-actions"].filter((name) => slots[name]));
|
|
57
|
+
const transcriptSlots = computed(() => ["welcome", "attachments", "system-message", "message-actions"].filter((name) => slots[name]));
|
|
57
58
|
const input = ref(null);
|
|
58
59
|
const delivery = ref(null);
|
|
59
60
|
function submit() {
|
|
@@ -74,5 +75,6 @@ defineExpose({ focus: () => input.value?.focus(), submit, stop });
|
|
|
74
75
|
<style scoped>
|
|
75
76
|
.assistant-conversation { display: flex; flex-direction: column; min-height: 0; min-width: 0; height: 100%; gap: 0; }
|
|
76
77
|
.assistant-conversation__transcript { flex: 1 1 auto; min-height: 0; }
|
|
78
|
+
.assistant-conversation__composer { flex: 0 0 auto; }
|
|
77
79
|
.assistant-conversation__setting { min-width: 8rem; max-width: 16rem; }
|
|
78
80
|
</style>
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
<span>{{ assistantLabel }}</span>
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
75
|
-
<p>{{ welcomeMessage }}</p>
|
|
75
|
+
<slot name="welcome"><p>{{ welcomeMessage }}</p></slot>
|
|
76
76
|
</div>
|
|
77
77
|
|
|
78
78
|
<div
|
|
@@ -873,6 +873,11 @@ onBeforeUnmount(() => {
|
|
|
873
873
|
clearLoadMoreScrollSnapshot();
|
|
874
874
|
});
|
|
875
875
|
|
|
876
|
+
watch(() => props.scrollKey, () => {
|
|
877
|
+
expandedUserMessages.value = new Set();
|
|
878
|
+
displayMessageCache.clear();
|
|
879
|
+
});
|
|
880
|
+
|
|
876
881
|
watch(() => [
|
|
877
882
|
timelineScrollTrigger.value,
|
|
878
883
|
latestRenderedTailKey.value
|
|
@@ -1073,21 +1078,21 @@ watch(timelineScrollTrigger, () => {
|
|
|
1073
1078
|
}
|
|
1074
1079
|
|
|
1075
1080
|
.assistant-transcript__avatar--user {
|
|
1076
|
-
background:
|
|
1077
|
-
color:
|
|
1081
|
+
background: rgba(var(--v-theme-primary), 0.1);
|
|
1082
|
+
color: rgb(var(--v-theme-primary));
|
|
1078
1083
|
margin-top: 0.2rem;
|
|
1079
1084
|
}
|
|
1080
1085
|
|
|
1081
1086
|
.assistant-transcript__avatar--assistant {
|
|
1082
|
-
background:
|
|
1083
|
-
color:
|
|
1087
|
+
background: rgb(var(--v-theme-primary));
|
|
1088
|
+
color: rgb(var(--v-theme-on-primary));
|
|
1084
1089
|
margin-top: 0.05rem;
|
|
1085
1090
|
}
|
|
1086
1091
|
|
|
1087
1092
|
.assistant-transcript__message--user {
|
|
1088
|
-
background:
|
|
1093
|
+
background: rgba(var(--v-theme-primary), 0.1);
|
|
1089
1094
|
border-radius: 16px;
|
|
1090
|
-
color:
|
|
1095
|
+
color: rgb(var(--v-theme-on-surface));
|
|
1091
1096
|
gap: 0.75rem;
|
|
1092
1097
|
justify-content: space-between;
|
|
1093
1098
|
overflow-x: auto;
|
package/src/client/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export { default as AssistantClientElement } from "./components/AssistantClientElement.vue";
|
|
2
1
|
export { default as AssistantSettingsFormCard } from "./components/AssistantSettingsFormCard.vue";
|
|
3
2
|
export { assistantHttpClient } from "./lib/assistantHttpClient.js";
|
|
4
3
|
export { createAssistantApi, buildStreamEventError } from "./lib/assistantApi.js";
|
|
5
|
-
export { renderMarkdownToSafeHtml } from "./lib/markdownRenderer.js";
|
|
@@ -14,94 +14,31 @@ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
|
14
14
|
const FIXTURE_ROOT = path.resolve(TEST_DIRECTORY, "../fixtures/responsive-assistant");
|
|
15
15
|
const RUN_BROWSER_TEST = process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION === "1";
|
|
16
16
|
const VIEWPORTS = Object.freeze([
|
|
17
|
-
Object.freeze({ name: "phone", width: 390, height: 844
|
|
18
|
-
Object.freeze({ name: "compact", width: 800, height: 900
|
|
19
|
-
Object.freeze({ name: "medium", width: 1224, height: 900
|
|
20
|
-
Object.freeze({ name: "expanded", width: 1365, height: 900
|
|
17
|
+
Object.freeze({ name: "phone", width: 390, height: 844 }),
|
|
18
|
+
Object.freeze({ name: "compact", width: 800, height: 900 }),
|
|
19
|
+
Object.freeze({ name: "medium", width: 1224, height: 900 }),
|
|
20
|
+
Object.freeze({ name: "expanded", width: 1365, height: 900 }),
|
|
21
|
+
Object.freeze({ name: "short", width: 1280, height: 600 }),
|
|
22
|
+
Object.freeze({ name: "drawer", width: 360, height: 600 })
|
|
21
23
|
]);
|
|
22
24
|
|
|
23
|
-
async function readLayoutMetrics(page) {
|
|
24
|
-
return page.evaluate(() => {
|
|
25
|
-
const root = document.querySelector(".assistant-client-element");
|
|
26
|
-
const layout = document.querySelector(".assistant-layout");
|
|
27
|
-
const mainColumn = document.querySelector(".assistant-main-col");
|
|
28
|
-
const sideColumn = document.querySelector(".assistant-side-col");
|
|
29
|
-
const messages = document.querySelector(".messages-panel");
|
|
30
|
-
const composer = document.querySelector(".assistant-composer-shell");
|
|
31
|
-
const history = document.querySelector(".assistant-history-card");
|
|
32
|
-
const tools = document.querySelector(".assistant-tools-card");
|
|
33
|
-
const rect = (element) => element?.getBoundingClientRect() || null;
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
viewportHeight: window.innerHeight,
|
|
37
|
-
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
38
|
-
root: rect(root),
|
|
39
|
-
layout: rect(layout),
|
|
40
|
-
mainColumn: rect(mainColumn),
|
|
41
|
-
sideColumn: rect(sideColumn),
|
|
42
|
-
messages: {
|
|
43
|
-
...rect(messages),
|
|
44
|
-
clientHeight: messages?.clientHeight || 0,
|
|
45
|
-
scrollHeight: messages?.scrollHeight || 0,
|
|
46
|
-
scrollTop: messages?.scrollTop || 0
|
|
47
|
-
},
|
|
48
|
-
composer: rect(composer),
|
|
49
|
-
sideColumnDisplay: sideColumn ? window.getComputedStyle(sideColumn).display : "missing",
|
|
50
|
-
historyDisplay: history ? window.getComputedStyle(history).display : "missing",
|
|
51
|
-
toolsDisplay: tools ? window.getComputedStyle(tools).display : "missing"
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
25
|
async function assertResponsiveAssistant(page, viewport) {
|
|
57
26
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
58
|
-
await page.goto("/"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
await
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
assert.ok(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
assert.ok(metrics.composer, `${viewport.name}: composer was not rendered.`);
|
|
71
|
-
assert.ok(
|
|
72
|
-
metrics.composer.bottom <= metrics.viewportHeight + 1,
|
|
73
|
-
`${viewport.name}: composer bottom ${metrics.composer.bottom}px exceeded the ${metrics.viewportHeight}px viewport.`
|
|
74
|
-
);
|
|
75
|
-
assert.ok(
|
|
76
|
-
metrics.root.bottom <= metrics.viewportHeight + 1,
|
|
77
|
-
`${viewport.name}: assistant root escaped the viewport.`
|
|
78
|
-
);
|
|
79
|
-
assert.ok(
|
|
80
|
-
metrics.mainColumn.bottom <= metrics.root.bottom + 1,
|
|
81
|
-
`${viewport.name}: main column escaped its bounded root.`
|
|
82
|
-
);
|
|
83
|
-
assert.ok(metrics.documentOverflow <= 1, `${viewport.name}: page overflowed horizontally.`);
|
|
84
|
-
|
|
85
|
-
if (viewport.sidePanelsVisible) {
|
|
86
|
-
assert.equal(metrics.sideColumnDisplay, "flex", `${viewport.name}: sidebar column was hidden.`);
|
|
87
|
-
assert.equal(metrics.historyDisplay, "flex", `${viewport.name}: conversation history was hidden.`);
|
|
88
|
-
assert.equal(metrics.toolsDisplay, "flex", `${viewport.name}: tool timeline was hidden.`);
|
|
89
|
-
await expect(compactConversationControl).toBeHidden();
|
|
90
|
-
} else {
|
|
91
|
-
assert.equal(metrics.sideColumnDisplay, "none", `${viewport.name}: sidebar column still occupied the layout.`);
|
|
92
|
-
await expect(compactConversationControl).toBeVisible();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await messages.evaluate((element) => {
|
|
96
|
-
element.scrollTop = 0;
|
|
97
|
-
element.dispatchEvent(new Event("scroll"));
|
|
98
|
-
});
|
|
99
|
-
await messages.hover();
|
|
100
|
-
await page.mouse.wheel(0, 420);
|
|
101
|
-
await expect.poll(() => messages.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
|
|
27
|
+
await page.goto("/");
|
|
28
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
29
|
+
await expect(input).toBeVisible();
|
|
30
|
+
const body = page.locator(".assistant-transcript__body");
|
|
31
|
+
await expect(page.getByText("Conversation line 70:", { exact: false })).toBeAttached();
|
|
32
|
+
await expect.poll(() => body.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true);
|
|
33
|
+
const bounds = await input.boundingBox();
|
|
34
|
+
assert.ok(bounds.y + bounds.height <= viewport.height);
|
|
35
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
36
|
+
await body.hover();
|
|
37
|
+
await page.mouse.wheel(0, -600);
|
|
38
|
+
await expect.poll(() => body.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeGreaterThan(100);
|
|
102
39
|
}
|
|
103
40
|
|
|
104
|
-
test("
|
|
41
|
+
test("AssistantConversationElement keeps long conversations scrollable across responsive layouts", {
|
|
105
42
|
skip: RUN_BROWSER_TEST
|
|
106
43
|
? false
|
|
107
44
|
: "set JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION=1 to run assistant-core browser integration",
|
|
@@ -138,6 +75,45 @@ test("AssistantClientElement keeps long conversations scrollable across responsi
|
|
|
138
75
|
}
|
|
139
76
|
});
|
|
140
77
|
|
|
78
|
+
test("conversation scrolling preserves history anchors and resets expansion on selection", {
|
|
79
|
+
skip: !RUN_BROWSER_TEST,
|
|
80
|
+
timeout: 60_000
|
|
81
|
+
}, async () => {
|
|
82
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
83
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
84
|
+
try {
|
|
85
|
+
const page = await browser.newPage();
|
|
86
|
+
await page.goto(`${vite.baseURL}/?controls=1`);
|
|
87
|
+
const body = page.locator(".assistant-transcript__body");
|
|
88
|
+
await expect(page.getByText("Conversation line 70:", { exact: false })).toBeVisible();
|
|
89
|
+
await body.hover();
|
|
90
|
+
await page.mouse.wheel(0, -100_000);
|
|
91
|
+
await expect.poll(() => body.evaluate((element) => element.scrollTop)).toBe(0);
|
|
92
|
+
await page.getByRole("button", { name: "Read more", exact: true }).click();
|
|
93
|
+
await expect(page.getByRole("button", { name: "Show less", exact: true })).toBeVisible();
|
|
94
|
+
const original = page.getByText("Conversation line 1:", { exact: false }).first();
|
|
95
|
+
const originalTop = (await original.boundingBox()).y;
|
|
96
|
+
await page.getByRole("button", { name: "Load older messages", exact: true }).click();
|
|
97
|
+
await expect.poll(async () => Math.abs((await original.boundingBox()).y - originalTop)).toBeLessThan(2);
|
|
98
|
+
const before = await body.evaluate((element) => element.scrollTop);
|
|
99
|
+
await page.getByRole("button", { name: "Append reply", exact: true }).click();
|
|
100
|
+
await expect(page.getByText("New reply.", { exact: false })).toBeAttached();
|
|
101
|
+
assert.equal(await body.evaluate((element) => element.scrollTop), before);
|
|
102
|
+
await body.hover();
|
|
103
|
+
await page.mouse.wheel(0, 100_000);
|
|
104
|
+
await expect.poll(() => body.evaluate((element) => element.scrollHeight - element.scrollTop - element.clientHeight)).toBeLessThan(2);
|
|
105
|
+
await page.getByRole("button", { name: "Append reply", exact: true }).click();
|
|
106
|
+
await expect(page.getByText("New reply.", { exact: false })).toHaveCount(2);
|
|
107
|
+
await expect.poll(() => body.evaluate((element) => element.scrollHeight - element.scrollTop - element.clientHeight)).toBeLessThan(2);
|
|
108
|
+
await page.getByRole("button", { name: "Change conversation", exact: true }).click();
|
|
109
|
+
await expect(page.getByRole("button", { name: "Show less", exact: true })).toHaveCount(0);
|
|
110
|
+
await expect(page.getByRole("button", { name: "Read more", exact: true })).toBeAttached();
|
|
111
|
+
} finally {
|
|
112
|
+
await browser.close();
|
|
113
|
+
await stopProcess(vite);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
141
117
|
test("conversation composer preserves typing and reacts to external state and pane resizing", {
|
|
142
118
|
skip: !RUN_BROWSER_TEST,
|
|
143
119
|
timeout: 60_000
|
|
@@ -168,6 +144,13 @@ test("conversation composer preserves typing and reacts to external state and pa
|
|
|
168
144
|
const before = await input.evaluate((element) => element.clientHeight);
|
|
169
145
|
await page.getByRole("button", { name: "Resize pane" }).evaluate((element) => element.click());
|
|
170
146
|
await expect.poll(() => input.evaluate((element) => element.clientHeight)).toBeGreaterThan(before);
|
|
147
|
+
await page.getByRole("button", { name: "Toggle action feedback" }).evaluate((element) => element.click());
|
|
148
|
+
const feedback = page.getByText("Describe what you want to change.", { exact: true });
|
|
149
|
+
await expect(feedback).toBeVisible();
|
|
150
|
+
const sendBounds = await page.getByRole("button", { name: "Send", exact: true }).boundingBox();
|
|
151
|
+
assert.ok(sendBounds.height >= 48, "Compact panes retain a 48px action target on desktop");
|
|
152
|
+
assert.ok((await feedback.boundingBox()).y >= sendBounds.y + sendBounds.height, "Narrow action feedback follows the button");
|
|
153
|
+
await expect(input).toBeFocused();
|
|
171
154
|
await input.dispatchEvent("keydown", { key: "Enter", isComposing: true });
|
|
172
155
|
await expect(input).toHaveValue(draft);
|
|
173
156
|
await input.press("Enter");
|
package/test/codexClient.test.js
CHANGED
|
@@ -80,3 +80,12 @@ for (const message of ["invalid JSON", "null"]) test(`unreadable output (${messa
|
|
|
80
80
|
await assert.rejects(connected, /closed before it opened/);
|
|
81
81
|
assert.equal(client.isOpen(), false);
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
test("transport failure preserves its error code for pending requests", async () => {
|
|
85
|
+
const client = new CodexAppServerJsonRpcClient({ endpoint: "ws://fixture", WebSocketImpl: Socket, maxMessageBytes: 100 });
|
|
86
|
+
await client.connect();
|
|
87
|
+
const result = client.request("read");
|
|
88
|
+
client.socket.emit("message", "x".repeat(101));
|
|
89
|
+
await assert.rejects(result, { code: "assistant_codex_app_server_message_too_large" });
|
|
90
|
+
assert.equal(client.isOpen(), false);
|
|
91
|
+
});
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const source = await readPackageFile("src/client/components/AssistantClientElement.vue");
|
|
14
|
-
assert.match(source, /normalizeObject\(unref\(props\.viewer\)\)/);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("AssistantClientElement keeps its responsive scroll ancestors height-bounded", async () => {
|
|
18
|
-
const source = await readPackageFile("src/client/components/AssistantClientElement.vue");
|
|
19
|
-
|
|
20
|
-
assert.match(source, /<v-row class="assistant-layout h-100 flex-grow-1 flex-nowrap my-0">/u);
|
|
21
|
-
assert.match(source, /<v-col cols="12" md="8" class="assistant-main-col/u);
|
|
22
|
-
assert.match(source, /<v-col cols="12" md="4" class="assistant-side-col d-none d-md-flex/u);
|
|
23
|
-
assert.match(source, /class="d-md-none"/u);
|
|
24
|
-
assert.match(source, /\.assistant-main-col,\s*\.assistant-side-col,[\s\S]*height: 100%;[\s\S]*max-height: 100%;/u);
|
|
25
|
-
assert.match(source, /\.messages-panel \{[\s\S]*flex: 1 1 auto;[\s\S]*min-height: 0;[\s\S]*overflow: auto;/u);
|
|
3
|
+
import { readFile, access } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
test("client entry points expose one conversation implementation and retain settings/API helpers", async () => {
|
|
6
|
+
const source = await readFile(new URL("../src/client/index.js", import.meta.url), "utf8");
|
|
7
|
+
assert.doesNotMatch(source, /AssistantClientElement/);
|
|
8
|
+
assert.match(source, /AssistantSettingsFormCard/);
|
|
9
|
+
assert.match(source, /createAssistantApi/);
|
|
10
|
+
await assert.rejects(access(new URL("../src/client/components/AssistantClientElement.vue", import.meta.url)));
|
|
11
|
+
const conversation = await readFile(new URL("../src/client/conversation/index.js", import.meta.url), "utf8");
|
|
12
|
+
assert.match(conversation, /AssistantConversationElement/);
|
|
26
13
|
});
|