@jskit-ai/assistant-core 0.1.217 → 0.1.219
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/README.md +10 -0
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/DeliveryFixture.vue +15 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantTranscript.vue +17 -13
- package/test/codexConversationSession.test.js +3 -1
- package/test/messageDelivery.browser.test.js +29 -0
|
@@ -75,3 +75,13 @@ database or files; the UI has no storage assumptions.
|
|
|
75
75
|
|
|
76
76
|
See the JSKIT guide **Embeddable assistant conversations** for the complete
|
|
77
77
|
client, storage and provider contracts and reusable storage-adapter checks.
|
|
78
|
+
|
|
79
|
+
An adapter that sets `conversation.cancellableDelivery` and supplies
|
|
80
|
+
`actions.cancel(messageId)` also exposes **Cancel** next to **Retry** for uncertain
|
|
81
|
+
delivery. The application owns that cancellation and preserves accepted history.
|
|
82
|
+
Standalone delivery continues to offer cancellation only for definite failures.
|
|
83
|
+
Direct `AssistantTranscript` consumers opt in with `cancellableDelivery`.
|
|
84
|
+
|
|
85
|
+
The shared transcript interleaves timestamped messages across storage and native
|
|
86
|
+
streaming turns. Equal timestamps keep source order; undated notices keep their
|
|
87
|
+
slots. Reasoning groups form after ordering, preserving original turn identities.
|
|
@@ -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.219",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.239",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.241",
|
|
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.58",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.58"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -3,6 +3,7 @@ import { reactive, ref } from "vue";
|
|
|
3
3
|
import AssistantConversationElement from "../../src/client/conversation/AssistantConversationElement.vue";
|
|
4
4
|
import { createAssistantMessageDelivery } from "../../src/client/conversation/messageDelivery.js";
|
|
5
5
|
|
|
6
|
+
const recoveryFixture = new URLSearchParams(location.search).has("recovery");
|
|
6
7
|
const draft = ref("");
|
|
7
8
|
const turns = ref([]);
|
|
8
9
|
const requests = ref([]);
|
|
@@ -29,11 +30,24 @@ function accept() {
|
|
|
29
30
|
assistant: { text: "Accepted once." } }];
|
|
30
31
|
pendingRequest.resolve({ ok: true });
|
|
31
32
|
}
|
|
33
|
+
if (recoveryFixture) {
|
|
34
|
+
const at = minute => `2026-10-13T06:${minute}:00.000Z`;
|
|
35
|
+
turns.value = [
|
|
36
|
+
{ turnId: "one", user: { messageId: "one", text: "Question one", at: at("01") } },
|
|
37
|
+
{ turnId: "two", user: { messageId: "two", text: "Question two", at: at("03") } },
|
|
38
|
+
{ turnId: "native", messages: [
|
|
39
|
+
{ messageId: "reply-one", role: "assistant", text: "Reply one", at: at("02") },
|
|
40
|
+
{ messageId: "reply-two", role: "assistant", text: "Reply two", at: at("04") }
|
|
41
|
+
] }
|
|
42
|
+
];
|
|
43
|
+
delivery.state.messages.push({ id: "uncertain", text: "Discard failed attempt", status: "uncertain", error: "Lost response", createdAt: at("05") });
|
|
44
|
+
}
|
|
32
45
|
const adapter = reactive({
|
|
33
46
|
delivery, attachments,
|
|
34
|
-
conversation: { turns, visible: true, scrollKey: "delivery", welcomeMessage: "Start a conversation." },
|
|
47
|
+
conversation: { cancellableDelivery: recoveryFixture, turns, visible: true, scrollKey: "delivery", welcomeMessage: "Start a conversation." },
|
|
35
48
|
composer: { draft, canSend: true, submitOnEnter: true, queueWhileSending },
|
|
36
49
|
actions: {
|
|
50
|
+
...(recoveryFixture ? { cancel: id => delivery.remove(id) } : {}),
|
|
37
51
|
setDraft(value) { draft.value = value; }
|
|
38
52
|
}
|
|
39
53
|
});
|
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.219",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
48
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
49
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
50
|
-
"@jskit-ai/resource-core": "0.1.
|
|
51
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.183",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.183",
|
|
52
52
|
"@mdi/js": "^7.4.47",
|
|
53
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
54
54
|
"ai": "^7.0.101",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"ws": "^8.20.1"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.239",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.241",
|
|
62
62
|
"vue": "^3.5.13",
|
|
63
63
|
"vuetify": "^4.0.0"
|
|
64
64
|
},
|
|
@@ -196,12 +196,12 @@
|
|
|
196
196
|
{{ entry.delivery?.delivery.checking ? 'Checking…' : 'Retry' }}
|
|
197
197
|
</v-btn>
|
|
198
198
|
<v-btn
|
|
199
|
-
v-if="!entry.delivery"
|
|
199
|
+
v-if="!entry.delivery || cancellableDelivery"
|
|
200
200
|
class="assistant-transcript__delivery-action"
|
|
201
201
|
size="x-small"
|
|
202
202
|
type="button"
|
|
203
203
|
variant="text"
|
|
204
|
-
@click="emit('cancel-turn', entry.turn.optimistic.id)"
|
|
204
|
+
@click="emit('cancel-turn', entry.delivery?.delivery.messageId || entry.turn.optimistic.id)"
|
|
205
205
|
>
|
|
206
206
|
Cancel
|
|
207
207
|
</v-btn>
|
|
@@ -338,6 +338,7 @@ function previewForTurn(turn) {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
const props = defineProps({
|
|
341
|
+
cancellableDelivery: { type: Boolean, default: false },
|
|
341
342
|
systemLabel: { type: String, default: "Status" },
|
|
342
343
|
previewMessage: { type: Object, default: null },
|
|
343
344
|
working: { type: Boolean, default: undefined },
|
|
@@ -586,7 +587,7 @@ function conversationMessageKey(message = {}, index = 0) {
|
|
|
586
587
|
// Storage rows are not visual boundaries. Only an intervening message ends a
|
|
587
588
|
// reasoning group, including when history is prepended or execution continues.
|
|
588
589
|
const displayEntries = computed(() => {
|
|
589
|
-
const
|
|
590
|
+
const candidates = [];
|
|
590
591
|
for (const [index, turn] of props.turns.entries()) {
|
|
591
592
|
const turnId = String(turn.turnId || index + 1);
|
|
592
593
|
// A delivery notice belongs to its exact user message, not the previous reply.
|
|
@@ -606,18 +607,21 @@ const displayEntries = computed(() => {
|
|
|
606
607
|
previewUserMessage: role === "user"
|
|
607
608
|
}, key);
|
|
608
609
|
if (!displayed) continue;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
previous.messages.push({ ...displayed, key });
|
|
612
|
-
} else {
|
|
613
|
-
entries.push({
|
|
614
|
-
key, role, turn: { ...turn, turnId }, message: displayed,
|
|
615
|
-
delivery: role === "user" ? userDelivery : null,
|
|
616
|
-
...(role === "thinking" ? { messages: [{ ...displayed, key }] } : {})
|
|
617
|
-
});
|
|
618
|
-
}
|
|
610
|
+
candidates.push({ key, role, turn: { ...turn, turnId }, message: displayed,
|
|
611
|
+
delivery: role === "user" ? userDelivery : null });
|
|
619
612
|
}
|
|
620
613
|
}
|
|
614
|
+
// Native streaming turns and saved authored turns can arrive independently.
|
|
615
|
+
// Only dated entries participate; undated notices retain their original slot.
|
|
616
|
+
const dated = candidates.filter(entry => Number.isFinite(Date.parse(entry.message.at)))
|
|
617
|
+
.sort((a, b) => Date.parse(a.message.at) - Date.parse(b.message.at))[Symbol.iterator]();
|
|
618
|
+
const entries = [];
|
|
619
|
+
for (const candidate of candidates) {
|
|
620
|
+
const entry = Number.isFinite(Date.parse(candidate.message.at)) ? dated.next().value : candidate;
|
|
621
|
+
const previous = entries.at(-1);
|
|
622
|
+
if (entry.role === "thinking" && previous?.role === "thinking") previous.messages.push({ ...entry.message, key: entry.key });
|
|
623
|
+
else entries.push({ ...entry, ...(entry.role === "thinking" ? { messages: [{ ...entry.message, key: entry.key }] } : {}) });
|
|
624
|
+
}
|
|
621
625
|
// Anchor a progress group to the following message, or to the transcript end.
|
|
622
626
|
// Appending progress or prepending history then preserves its expansion state.
|
|
623
627
|
for (const [index, entry] of entries.entries()) {
|
|
@@ -847,7 +847,9 @@ async function select(conversation, engine, operationId) {
|
|
|
847
847
|
}
|
|
848
848
|
|
|
849
849
|
async function fixture(t, options = {}) {
|
|
850
|
-
|
|
850
|
+
// Keep enough room for the runtime's nested Unix socket path even when the
|
|
851
|
+
// host supplies a comparatively long temporary-directory prefix.
|
|
852
|
+
const directory = await mkdtemp(path.join(tmpdir(), "jc-"));
|
|
851
853
|
const command = path.join(directory, "codex.mjs");
|
|
852
854
|
const account = path.join(directory, "account.txt");
|
|
853
855
|
const trace = path.join(directory, "trace.jsonl");
|
|
@@ -90,3 +90,32 @@ test("the shared element renders delayed sends and supplies retry, edit and canc
|
|
|
90
90
|
await stopProcess(vite);
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
test("chronological messages and application Cancel work on phone and desktop", {
|
|
96
|
+
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
97
|
+
}, async () => {
|
|
98
|
+
const vite = await startViteFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/responsive-assistant", import.meta.url)) });
|
|
99
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
100
|
+
try {
|
|
101
|
+
for (const width of [390, 1365]) {
|
|
102
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
103
|
+
const errors = [];
|
|
104
|
+
page.on("pageerror", error => errors.push(error.message));
|
|
105
|
+
await page.goto(`${vite.baseURL}/?delivery=1&recovery=1`);
|
|
106
|
+
const rows = page.locator(".assistant-transcript__turn");
|
|
107
|
+
await expect(rows).toHaveCount(5);
|
|
108
|
+
for (const [index, text] of ["Question one", "Reply one", "Question two", "Reply two", "Discard failed attempt"].entries()) {
|
|
109
|
+
await expect(rows.nth(index)).toContainText(text);
|
|
110
|
+
}
|
|
111
|
+
await expect(page.getByRole("button", { name: "Retry delivery check" })).toBeVisible();
|
|
112
|
+
await page.screenshot({ path: `/tmp/sas-chat-recovery-${width}.png` });
|
|
113
|
+
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
|
114
|
+
await expect(rows).toHaveCount(4);
|
|
115
|
+
await expect(page.getByText("Discard failed attempt", { exact: true })).toHaveCount(0);
|
|
116
|
+
assert.deepEqual(errors, []);
|
|
117
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
118
|
+
await page.close();
|
|
119
|
+
}
|
|
120
|
+
} finally { await browser.close(); await stopProcess(vite); }
|
|
121
|
+
});
|