@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.
@@ -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.217",
12
- "@jskit-ai/http-runtime": "0.1.237",
13
- "@jskit-ai/kernel": "0.1.239",
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.56",
18
- "@jskit-ai/connectors-catalog": "0.1.56"
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.217",
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.181",
51
- "@jskit-ai/resource-crud-core": "0.1.181",
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.237",
61
- "@jskit-ai/kernel": "0.1.239",
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 entries = [];
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
- const previous = entries.at(-1);
610
- if (role === "thinking" && previous?.role === "thinking") {
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
- const directory = await mkdtemp(path.join(tmpdir(), "jskit-codex-conversation-"));
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
+ });