@jskit-ai/assistant-core 0.1.215 → 0.1.217

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.217",
12
+ "@jskit-ai/http-runtime": "0.1.237",
13
+ "@jskit-ai/kernel": "0.1.239",
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.56",
18
+ "@jskit-ai/connectors-catalog": "0.1.56"
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.217",
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.181",
51
+ "@jskit-ai/resource-crud-core": "0.1.181",
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.237",
61
+ "@jskit-ai/kernel": "0.1.239",
62
62
  "vue": "^3.5.13",
63
63
  "vuetify": "^4.0.0"
64
64
  },
@@ -1,14 +1,15 @@
1
1
  <template>
2
- <div v-if="message.delivery" role="status">
3
- <p>{{ message.text }}</p>
4
- <p v-if="message.delivery.error" class="text-body-small">{{ message.delivery.error }}</p>
2
+ <div v-if="message.delivery" class="d-flex flex-wrap align-center ga-1 text-caption text-error" role="status">
3
+ <span>{{ message.delivery.error || message.text }}</span>
5
4
  <v-btn
6
- variant="tonal"
7
- min-height="48"
5
+ variant="text"
6
+ color="error"
7
+ size="x-small"
8
+ aria-label="Retry delivery check"
8
9
  :disabled="message.delivery.checking"
9
10
  @click="$emit('check-delivery', message.delivery.messageId)"
10
11
  >
11
- {{ message.delivery.checking ? 'Checking delivery…' : 'Check delivery' }}
12
+ {{ message.delivery.checking ? 'Checking…' : 'Retry' }}
12
13
  </v-btn>
13
14
  </div>
14
15
  <slot v-else>
@@ -177,22 +177,26 @@
177
177
  </div>
178
178
  <slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
179
179
  <div
180
- v-if="entry.turn.optimistic?.status === 'failed'"
180
+ v-if="entry.delivery || entry.turn.optimistic?.status === 'failed'"
181
181
  class="assistant-transcript__optimistic-failure"
182
182
  >
183
- <span role="status">Failed: {{ entry.turn.optimistic.error || "Message could not be sent." }}</span>
183
+ <span role="status">{{ entry.delivery ? entry.delivery.delivery.error || 'Delivery unconfirmed.' : `Failed: ${entry.turn.optimistic.error || 'Message could not be sent.'}` }}</span>
184
184
  <div v-if="!entry.turn.preview" class="assistant-transcript__optimistic-actions">
185
185
  <v-btn
186
186
  class="assistant-transcript__delivery-action"
187
- color="primary"
187
+ color="error"
188
188
  size="x-small"
189
189
  type="button"
190
- variant="tonal"
191
- @click="emit('resend-turn', entry.turn.optimistic.id)"
190
+ variant="text"
191
+ :disabled="entry.delivery?.delivery.checking"
192
+ :aria-label="entry.delivery ? 'Retry delivery check' : undefined"
193
+ :title="entry.delivery ? entry.delivery.delivery.error || 'Check delivery before resending.' : undefined"
194
+ @click="entry.delivery ? emit('check-delivery', entry.delivery.delivery.messageId) : emit('resend-turn', entry.turn.optimistic.id)"
192
195
  >
193
- Retry
196
+ {{ entry.delivery?.delivery.checking ? 'Checking…' : 'Retry' }}
194
197
  </v-btn>
195
198
  <v-btn
199
+ v-if="!entry.delivery"
196
200
  class="assistant-transcript__delivery-action"
197
201
  size="x-small"
198
202
  type="button"
@@ -202,6 +206,7 @@
202
206
  Cancel
203
207
  </v-btn>
204
208
  <v-btn
209
+ v-if="!entry.delivery"
205
210
  class="assistant-transcript__delivery-action"
206
211
  size="x-small"
207
212
  type="button"
@@ -584,8 +589,11 @@ const displayEntries = computed(() => {
584
589
  const entries = [];
585
590
  for (const [index, turn] of props.turns.entries()) {
586
591
  const turnId = String(turn.turnId || index + 1);
592
+ // A delivery notice belongs to its exact user message, not the previous reply.
593
+ const userDelivery = turn.user && turn.system?.delivery && turn.system.delivery.messageId === turn.user.messageId
594
+ ? turn.system : null;
587
595
  const messages = [
588
- turn.system && { ...turn.system, role: "system" },
596
+ turn.system && !userDelivery && { ...turn.system, role: "system" },
589
597
  turn.user && { ...turn.user, role: "user" },
590
598
  ...conversationAgentMessages(turn)
591
599
  ].filter(Boolean);
@@ -604,6 +612,7 @@ const displayEntries = computed(() => {
604
612
  } else {
605
613
  entries.push({
606
614
  key, role, turn: { ...turn, turnId }, message: displayed,
615
+ delivery: role === "user" ? userDelivery : null,
607
616
  ...(role === "thinking" ? { messages: [{ ...displayed, key }] } : {})
608
617
  });
609
618
  }
@@ -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
+ });