@jskit-ai/assistant-core 0.1.215 → 0.1.216

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.
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.1.215",
12
- "@jskit-ai/http-runtime": "0.1.235",
13
- "@jskit-ai/kernel": "0.1.237",
11
+ "@jskit-ai/assistant-core": "0.1.216",
12
+ "@jskit-ai/http-runtime": "0.1.236",
13
+ "@jskit-ai/kernel": "0.1.238",
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.54",
18
- "@jskit-ai/connectors-catalog": "0.1.54"
17
+ "@jskit-ai/connectors-core": "0.1.55",
18
+ "@jskit-ai/connectors-catalog": "0.1.55"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.1.215",
3
+ "version": "0.1.216",
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.179",
51
- "@jskit-ai/resource-crud-core": "0.1.179",
50
+ "@jskit-ai/resource-core": "0.1.180",
51
+ "@jskit-ai/resource-crud-core": "0.1.180",
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.235",
61
- "@jskit-ai/kernel": "0.1.237",
60
+ "@jskit-ai/http-runtime": "0.1.236",
61
+ "@jskit-ai/kernel": "0.1.238",
62
62
  "vue": "^3.5.13",
63
63
  "vuetify": "^4.0.0"
64
64
  },
@@ -139,7 +139,8 @@
139
139
  class="assistant-transcript__message assistant-transcript__message--user"
140
140
  :class="{
141
141
  'assistant-transcript__message--pending': entry.turn.optimistic?.status === 'pending',
142
- 'assistant-transcript__message--failed': entry.turn.optimistic?.status === 'failed'
142
+ 'assistant-transcript__message--failed': entry.turn.optimistic?.status === 'failed',
143
+ 'assistant-transcript__message--unconfirmed': Boolean(entry.delivery)
143
144
  }"
144
145
  >
145
146
  <div
@@ -175,6 +176,12 @@
175
176
  <time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
176
177
  <span v-if="entry.turn.optimistic?.status === 'pending'" role="status">Pending</span>
177
178
  </div>
179
+ <div v-if="entry.delivery" class="assistant-transcript__unconfirmed-delivery">
180
+ <AssistantConversationStatus
181
+ :message="{ ...entry.delivery, text: 'Delivery unconfirmed' }"
182
+ @check-delivery="emit('check-delivery', $event)"
183
+ />
184
+ </div>
178
185
  <slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
179
186
  <div
180
187
  v-if="entry.turn.optimistic?.status === 'failed'"
@@ -584,8 +591,11 @@ const displayEntries = computed(() => {
584
591
  const entries = [];
585
592
  for (const [index, turn] of props.turns.entries()) {
586
593
  const turnId = String(turn.turnId || index + 1);
594
+ // A delivery notice belongs to its exact user message, not the previous reply.
595
+ const userDelivery = turn.user && turn.system?.delivery && turn.system.delivery.messageId === turn.user.messageId
596
+ ? turn.system : null;
587
597
  const messages = [
588
- turn.system && { ...turn.system, role: "system" },
598
+ turn.system && !userDelivery && { ...turn.system, role: "system" },
589
599
  turn.user && { ...turn.user, role: "user" },
590
600
  ...conversationAgentMessages(turn)
591
601
  ].filter(Boolean);
@@ -604,6 +614,7 @@ const displayEntries = computed(() => {
604
614
  } else {
605
615
  entries.push({
606
616
  key, role, turn: { ...turn, turnId }, message: displayed,
617
+ delivery: role === "user" ? userDelivery : null,
607
618
  ...(role === "thinking" ? { messages: [{ ...displayed, key }] } : {})
608
619
  });
609
620
  }
@@ -1431,6 +1442,26 @@ watch(() => props.visible, (visible) => {
1431
1442
  margin-top: -0.15rem;
1432
1443
  }
1433
1444
 
1445
+ .assistant-transcript__message--user.assistant-transcript__message--unconfirmed {
1446
+ background: rgba(var(--v-theme-warning), 0.08);
1447
+ border: 1px solid rgba(var(--v-theme-warning), 0.6);
1448
+ }
1449
+
1450
+ .assistant-transcript__unconfirmed-delivery {
1451
+ border-top: 1px solid rgba(var(--v-theme-warning), 0.4);
1452
+ font-size: 0.875rem;
1453
+ padding-top: 0.65rem;
1454
+ }
1455
+
1456
+ .assistant-transcript__unconfirmed-delivery :deep(p:first-child) {
1457
+ font-weight: 700;
1458
+ margin-bottom: 0.4rem;
1459
+ }
1460
+
1461
+ .assistant-transcript__unconfirmed-delivery :deep(p) {
1462
+ overflow-wrap: anywhere;
1463
+ }
1464
+
1434
1465
  .assistant-transcript__optimistic-failure {
1435
1466
  align-items: center;
1436
1467
  color: rgba(var(--v-theme-error), 0.92);
@@ -111,6 +111,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
111
111
  const ownerPendingSends = pendingSends;
112
112
  const current = () => pendingSends === ownerPendingSends && isCurrent();
113
113
  const predecessor = deliveryTail;
114
+ const knownUncertain = new Set(state.messages.filter(entry => entry.status === "uncertain").map(entry => entry.id));
114
115
  const settled = Promise.withResolvers();
115
116
  deliveryTail = settled.promise;
116
117
  ownerPendingSends.set(messageId, settled);
@@ -132,7 +133,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
132
133
  try {
133
134
  if (predecessor) await predecessor;
134
135
  if (!current()) return false;
135
- if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain")) {
136
+ if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain" && !knownUncertain.has(entry.id))) {
136
137
  fail(messageId, "Check the previous message's delivery before sending again.");
137
138
  return false;
138
139
  }
@@ -3,7 +3,7 @@ import { computed, onScopeDispose, ref, toValue, unref, watch } from "vue";
3
3
  /** Suggestion requests use an application-supplied agent; credentials stay on its server. */
4
4
  export function useAssistantSuggestions({
5
5
  active = true, requestKey = "", draft = "", configuration = {},
6
- generate, onSelect = () => false, debounceMs = 750
6
+ generate, onSelect = () => false, debounceMs = 750, minIntervalMs = 0
7
7
  } = {}) {
8
8
  const items = ref([]);
9
9
  const loading = ref(false);
@@ -18,6 +18,7 @@ export function useAssistantSuggestions({
18
18
  const visible = computed(() => eligible.value && (loading.value || items.value.length > 0));
19
19
  let timer;
20
20
  let request;
21
+ let lastStartedAt = -Infinity;
21
22
 
22
23
  function cancel() {
23
24
  clearTimeout(timer);
@@ -43,6 +44,7 @@ export function useAssistantSuggestions({
43
44
  const run = unref(generate);
44
45
  loading.value = true;
45
46
  timer = setTimeout(async () => {
47
+ lastStartedAt = Date.now();
46
48
  try {
47
49
  const result = await run(input);
48
50
  if (request !== controller) return;
@@ -58,7 +60,8 @@ export function useAssistantSuggestions({
58
60
  loading.value = false;
59
61
  }
60
62
  }
61
- }, Math.max(0, Number(toValue(debounceMs)) || 0));
63
+ }, Math.max(0, Number(toValue(debounceMs)) || 0,
64
+ lastStartedAt + Math.max(0, Number(toValue(minIntervalMs)) || 0) - Date.now()));
62
65
  }, { immediate: true, flush: "sync" });
63
66
 
64
67
  function currentSuggestion(value) {
@@ -80,7 +83,6 @@ export function useAssistantSuggestions({
80
83
  }
81
84
  function focus() {
82
85
  if (toValue(active) === false || composerFocused.value) return;
83
- dismissedKey.value = null;
84
86
  composerFocused.value = true;
85
87
  }
86
88
  function blur() {
@@ -397,3 +397,18 @@ test("restored application requests retain their origin and attachment-only user
397
397
  assert.equal(delivery.turns()[0].user.text, "");
398
398
  assert.deepEqual(delivery.turns()[0].user.attachments, [{ attachmentId: "image" }]);
399
399
  });
400
+
401
+ test("a deliberate new message can follow known uncertainty without resending the old request", async () => {
402
+ const sent = [];
403
+ const delivery = createAssistantMessageDelivery({ deliver: async payload => {
404
+ sent.push(payload);
405
+ if (payload.messageId === "old") throw new Error("Lost receipt");
406
+ return { ok: true };
407
+ } });
408
+ await assert.rejects(delivery.send({ message: "Old request" }, { messageId: "old", uncertainOnError: true }));
409
+ assert.equal(await delivery.send({ message: "Old request" }, { messageId: "old", uncertainOnError: true }), false);
410
+ assert.deepEqual(await delivery.send({ message: "A new request" }, { messageId: "new", uncertainOnError: true }), { ok: true });
411
+ assert.deepEqual(sent.map(message => message.messageId), ["old", "new"]);
412
+ assert.equal(delivery.find("old").status, "uncertain");
413
+ assert.equal(delivery.find("new").status, "accepted");
414
+ });
@@ -87,7 +87,7 @@ test("suggestion previews preserve a draft and stale selections cannot submit",
87
87
  state.focus();
88
88
  t.mock.timers.tick(750);
89
89
  await flush();
90
- assert.equal(state.visible.value, true);
90
+ assert.equal(state.visible.value, false, "Refocusing does not regenerate dismissed suggestions");
91
91
  draft.value = "Keep my text";
92
92
  assert.equal(state.preview.value, "");
93
93
  assert.equal(state.select(suggestion("Next step")), false);
@@ -112,3 +112,36 @@ test("a failing suggestion agent leaves the composer independent and stops retry
112
112
  t.mock.timers.tick(60_000);
113
113
  assert.equal(requests, 1);
114
114
  });
115
+
116
+
117
+ test("suggestions wait five seconds and coalesce edited drafts through a thirty-second cooldown", async t => {
118
+ t.mock.timers.enable({ apis: ["setTimeout", "Date"], now: 1_000 });
119
+ const scope = effectScope();
120
+ t.after(() => scope.stop());
121
+ const draft = ref("First");
122
+ const active = ref(true);
123
+ const calls = [];
124
+ const state = scope.run(() => useAssistantSuggestions({ draft, active, debounceMs: 5_000, minIntervalMs: 30_000,
125
+ generate: async input => { calls.push({ draft: input.draft, at: Date.now() }); return [suggestion(input.draft)]; }
126
+ }));
127
+ t.mock.timers.tick(4_999);
128
+ state.focus(); state.blur(); state.focus();
129
+ assert.equal(calls.length, 0);
130
+ t.mock.timers.tick(1);
131
+ await flush();
132
+ draft.value = "Second";
133
+ t.mock.timers.tick(15_000);
134
+ draft.value = "Newest";
135
+ state.focus();
136
+ t.mock.timers.tick(14_999);
137
+ assert.equal(calls.length, 1);
138
+ t.mock.timers.tick(1);
139
+ await flush();
140
+ assert.deepEqual(calls, [{ draft: "First", at: 6_000 }, { draft: "Newest", at: 36_000 }]);
141
+ t.mock.timers.tick(90_000);
142
+ assert.equal(calls.length, 2, "Cooldown does not poll when the input is unchanged");
143
+ draft.value = "Cancelled before dispatch";
144
+ active.value = false;
145
+ t.mock.timers.tick(30_000);
146
+ assert.equal(calls.length, 2);
147
+ });