@jskit-ai/assistant-core 0.1.244 → 0.2.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/examples/conversation/package.json +5 -5
  3. package/locales/en.json +383 -0
  4. package/package.json +7 -6
  5. package/src/client/components/AssistantSettingsFormCard.vue +4 -1
  6. package/src/client/conversation/AssistantAttachmentPreview.vue +6 -3
  7. package/src/client/conversation/AssistantAttachmentQueue.vue +36 -37
  8. package/src/client/conversation/AssistantComposerActions.vue +6 -3
  9. package/src/client/conversation/AssistantComposerSupport.vue +8 -5
  10. package/src/client/conversation/AssistantConversationElement.vue +16 -13
  11. package/src/client/conversation/AssistantConversationStatus.vue +5 -2
  12. package/src/client/conversation/AssistantGoalControl.vue +22 -16
  13. package/src/client/conversation/AssistantMessageAttachments.vue +7 -4
  14. package/src/client/conversation/AssistantModelControl.vue +23 -20
  15. package/src/client/conversation/AssistantProgress.vue +6 -3
  16. package/src/client/conversation/AssistantQuestionInputs.vue +9 -6
  17. package/src/client/conversation/AssistantTranscript.vue +39 -28
  18. package/src/client/conversation/LongTextPreviewBlocks.vue +10 -2
  19. package/src/client/conversation/attachmentSize.js +38 -0
  20. package/src/client/conversation/messageDelivery.js +14 -5
  21. package/src/client/conversation/submitText.js +11 -4
  22. package/src/client/conversation/useAssistantAttachments.js +40 -30
  23. package/src/client/conversation/useAssistantSuggestions.js +4 -1
  24. package/src/client/i18n.js +6 -0
  25. package/src/client/lib/assistantApi.js +14 -2
  26. package/src/server/conversation/attachments.js +24 -14
  27. package/src/server/conversation/claudeProcess.js +4 -2
  28. package/src/server/conversation/claudeTurn.js +14 -11
  29. package/src/server/conversation/codexConversations.js +4 -0
  30. package/src/server/conversation/codexGoalCommands.js +9 -9
  31. package/src/server/conversation/codexHelperExecution.js +4 -0
  32. package/src/server/conversation/codexHelperThreads.js +2 -0
  33. package/src/server/conversation/codexMessageCommands.js +7 -1
  34. package/src/server/conversation/codexTurnControl.js +2 -1
  35. package/src/server/conversation/codexTurnState.js +2 -0
  36. package/src/server/conversation/continuity.js +16 -6
  37. package/src/server/conversation/runtime.js +193 -141
  38. package/src/server/lib/ndjson.js +19 -1
  39. package/src/server/lib/providers/common.js +7 -2
  40. package/src/shared/conversation/numberedQuestionSugar.js +7 -1
  41. package/src/shared/conversation/turns.js +10 -2
  42. package/test/i18nCatalog.test.js +22 -0
@@ -5,11 +5,11 @@
5
5
  class="assistant-attachment-queue"
6
6
  :class="{ 'assistant-attachment-queue--standalone': !joined }"
7
7
  :aria-busy="hasUnresolved ? 'true' : undefined"
8
- aria-label="Message attachments"
8
+ :aria-label="t('jskit.assistant.attachments.queue')"
9
9
  >
10
10
  <header class="assistant-attachment-queue__summary">
11
11
  <span>{{ aggregateLabel }}</span>
12
- <span class="assistant-attachment-queue__limit">{{ retainedItems.length }} / {{ maximumFiles }}</span>
12
+ <span class="assistant-attachment-queue__limit">{{ t("jskit.assistant.attachments.limit", { count: retainedItems.length, max: maximumFiles }) }}</span>
13
13
  </header>
14
14
 
15
15
  <ol class="assistant-attachment-queue__items">
@@ -38,7 +38,7 @@
38
38
  >
39
39
  {{ item.receipt?.reference }} {{ itemName(item) }}
40
40
  </component>
41
- <span class="assistant-attachment-queue__size">{{ attachmentSizeLabel(item.size) }}</span>
41
+ <span class="assistant-attachment-queue__size">{{ attachmentSizeText(item.size) }}</span>
42
42
  </div>
43
43
  <div class="assistant-attachment-queue__secondary">
44
44
  <span>{{ itemStatusLabel(item) }}</span>
@@ -51,7 +51,7 @@
51
51
  />
52
52
  <v-progress-linear
53
53
  v-else-if="itemProgressVisible(item)"
54
- :aria-label="`Upload progress for ${itemName(item)}`"
54
+ :aria-label="t('jskit.assistant.attachments.uploadProgress', { name: itemName(item) })"
55
55
  class="assistant-attachment-queue__progress"
56
56
  color="primary"
57
57
  height="3"
@@ -72,11 +72,11 @@
72
72
  <v-btn
73
73
  v-if="itemCancelable(item)"
74
74
  :ref="(element) => setActionElement(item, 'cancel', element)"
75
- :aria-label="`Cancel ${itemName(item)}`"
75
+ :aria-label="t('jskit.assistant.attachments.cancelItem', { name: itemName(item) })"
76
76
  class="assistant-attachment-queue__action"
77
77
  :icon="mdiCloseCircleOutline"
78
78
  size="small"
79
- :title="`Cancel ${itemName(item)}`"
79
+ :title="t('jskit.assistant.attachments.cancelItem', { name: itemName(item) })"
80
80
  type="button"
81
81
  variant="text"
82
82
  @click="handleAction('cancel', item, index)"
@@ -84,11 +84,11 @@
84
84
  <v-btn
85
85
  v-if="itemPhase(item) === 'failed' || itemPhase(item) === 'cancelled'"
86
86
  :ref="(element) => setActionElement(item, 'retry', element)"
87
- :aria-label="`Retry ${itemName(item)}`"
87
+ :aria-label="t('jskit.assistant.attachments.retryItem', { name: itemName(item) })"
88
88
  class="assistant-attachment-queue__action"
89
89
  :icon="mdiRefresh"
90
90
  size="small"
91
- :title="`Retry ${itemName(item)}`"
91
+ :title="t('jskit.assistant.attachments.retryItem', { name: itemName(item) })"
92
92
  type="button"
93
93
  variant="text"
94
94
  @click="handleAction('retry', item, index)"
@@ -96,11 +96,11 @@
96
96
  <v-btn
97
97
  v-if="itemPhase(item) === 'ready' || itemPhase(item) === 'failed' || itemPhase(item) === 'cancelled'"
98
98
  :ref="(element) => setActionElement(item, 'remove', element)"
99
- :aria-label="`Remove ${itemName(item)}`"
99
+ :aria-label="t('jskit.assistant.attachments.removeItem', { name: itemName(item) })"
100
100
  class="assistant-attachment-queue__action"
101
101
  :icon="mdiClose"
102
102
  size="small"
103
- :title="`Remove ${itemName(item)}`"
103
+ :title="t('jskit.assistant.attachments.removeItem', { name: itemName(item) })"
104
104
  type="button"
105
105
  variant="text"
106
106
  @click="handleAction('remove', item, index)"
@@ -131,7 +131,10 @@ import {
131
131
  mdiRefresh
132
132
  } from "@mdi/js";
133
133
 
134
- import { attachmentSizeLabel } from "../../shared/conversation/attachments.js";
134
+ import { useJskitText } from "../i18n.js";
135
+ import { attachmentSizeText } from "./attachmentSize.js";
136
+
137
+ const { t } = useJskitText();
135
138
 
136
139
  const emit = defineEmits(["cancel", "remove", "retry", "preview", "focus-input"]);
137
140
 
@@ -187,47 +190,43 @@ const aggregateLabel = computed(() => {
187
190
  const cancelled = props.items.filter((item) => itemPhase(item) === "cancelled").length;
188
191
  if (active) {
189
192
  return aggregatePercent.value === null
190
- ? `${completedCount.value} of ${total} ready · in progress`
191
- : `${completedCount.value} of ${total} ready · ${aggregatePercent.value}%`;
193
+ ? t("jskit.assistant.attachments.summary.inProgress", { completed: completedCount.value, total })
194
+ : t("jskit.assistant.attachments.summary.percent", { completed: completedCount.value, total, percent: aggregatePercent.value });
192
195
  }
193
196
  if (failed) {
194
- return `${completedCount.value} of ${total} ready · ${failed} ${failed === 1 ? "needs" : "need"} attention`;
197
+ return t("jskit.assistant.attachments.summary.needAttention", { completed: completedCount.value, total, count: failed });
195
198
  }
196
199
  if (cancelled) {
197
- return `${completedCount.value} ready · ${cancelled} cancelled`;
200
+ return t("jskit.assistant.attachments.summary.cancelled", { completed: completedCount.value, cancelled });
198
201
  }
199
- return `${completedCount.value} of ${total} ready`;
202
+ return t("jskit.assistant.attachments.summary.ready", { completed: completedCount.value, total });
200
203
  });
201
204
  const announcement = computed(() => {
202
205
  const messages = [];
203
206
  const failed = props.items.filter((item) => itemPhase(item) === "failed").length;
204
207
  if (failed) {
205
- messages.push(`${attachmentCountLabel(failed)} ${failed === 1 ? "needs" : "need"} attention.`);
208
+ messages.push(t("jskit.assistant.attachments.announcement.needAttention", { count: failed }));
206
209
  }
207
210
  const active = props.items.filter((item) => activePhases.has(itemPhase(item))).length;
208
211
  if (active) {
209
- messages.push(`${attachmentCountLabel(active)} ${active === 1 ? "is" : "are"} in progress.`);
212
+ messages.push(t("jskit.assistant.attachments.announcement.inProgress", { count: active }));
210
213
  }
211
214
  const cancelled = props.items.filter((item) => itemPhase(item) === "cancelled").length;
212
215
  if (cancelled) {
213
- messages.push(`${attachmentCountLabel(cancelled)} cancelled.`);
216
+ messages.push(t("jskit.assistant.attachments.announcement.cancelled", { count: cancelled }));
214
217
  }
215
218
  if (completedCount.value) {
216
- messages.push(`${attachmentCountLabel(completedCount.value)} ready.`);
219
+ messages.push(t("jskit.assistant.attachments.announcement.ready", { count: completedCount.value }));
217
220
  }
218
221
  return messages.join(" ");
219
222
  });
220
223
 
221
- function attachmentCountLabel(count = 0) {
222
- return `${count} attachment${count === 1 ? "" : "s"}`;
223
- }
224
-
225
224
  function itemKey(item = {}) {
226
225
  return String(item.clientId || item.id || item.attachmentId || item.fileName || "attachment");
227
226
  }
228
227
 
229
228
  function itemName(item = {}) {
230
- return String(item.fileName || item.file?.name || "attachment");
229
+ return String(item.fileName || item.file?.name || t("jskit.assistant.attachments.unnamed"));
231
230
  }
232
231
 
233
232
  function itemPhase(item = {}) {
@@ -278,25 +277,25 @@ function itemBytesLabel(item = {}) {
278
277
  const sent = Math.min(Math.max(0, Number(item.bytesSent) || 0), size);
279
278
  const percent = itemPercent(item);
280
279
  return percent === null
281
- ? `${attachmentSizeLabel(sent)} / ${attachmentSizeLabel(size)}`
282
- : `${attachmentSizeLabel(sent)} / ${attachmentSizeLabel(size)} · ${percent}%`;
280
+ ? t("jskit.assistant.attachments.bytes", { sent: attachmentSizeText(sent), size: attachmentSizeText(size) })
281
+ : t("jskit.assistant.attachments.bytesPercent", { sent: attachmentSizeText(sent), size: attachmentSizeText(size), percent });
283
282
  }
284
283
 
285
284
  function itemStatusLabel(item = {}) {
286
285
  const labels = {
287
- cancelled: "Cancelled",
288
- delivering: "Sending to Codex",
286
+ cancelled: "jskit.assistant.attachments.status.cancelled",
287
+ delivering: "jskit.assistant.attachments.status.delivering",
289
288
  failed: item.failureStage === "preparing"
290
- ? "Preparation failed"
289
+ ? "jskit.assistant.attachments.status.preparationFailed"
291
290
  : item.failureStage === "handoff"
292
- ? "Sending failed"
293
- : "Upload failed",
294
- preparing: "Preparing",
295
- queued: "Queued",
296
- ready: "Ready",
297
- uploading: "Uploading"
291
+ ? "jskit.assistant.attachments.status.sendingFailed"
292
+ : "jskit.assistant.attachments.status.uploadFailed",
293
+ preparing: "jskit.assistant.attachments.status.preparing",
294
+ queued: "jskit.assistant.attachments.status.queued",
295
+ ready: "jskit.assistant.attachments.status.ready",
296
+ uploading: "jskit.assistant.attachments.status.uploading"
298
297
  };
299
- return labels[itemPhase(item)] || "Queued";
298
+ return t(Object.hasOwn(labels, itemPhase(item)) ? labels[itemPhase(item)] : "jskit.assistant.attachments.status.queued");
300
299
  }
301
300
 
302
301
  function itemCancelable(item = {}) {
@@ -7,15 +7,15 @@
7
7
  :aria-busy="state.stopPending ? 'true' : undefined" :prepend-icon="mdiStopCircleOutline"
8
8
  size="small" variant="tonal" @click="emit('stop')"
9
9
  >
10
- {{ state.stopPending ? 'Stopping…' : 'Stop' }}
10
+ {{ state.stopPending ? t('jskit.assistant.composer.stopping') : t('jskit.assistant.composer.stop') }}
11
11
  </v-btn>
12
12
  <v-btn
13
13
  ref="sendButton" class="assistant-composer-actions__send" color="primary" :disabled="!state.canSend"
14
- :aria-busy="state.pending && !state.canSend ? 'true' : undefined" :aria-label="state.submitAriaLabel || state.submitLabel || 'Send message'"
14
+ :aria-busy="state.pending && !state.canSend ? 'true' : undefined" :aria-label="state.submitAriaLabel || state.submitLabel || t('jskit.assistant.composer.sendMessage')"
15
15
  :title="state.submitTitle" :prepend-icon="state.submitIcon || mdiSend" size="small" variant="flat"
16
16
  @click="emit('submit')"
17
17
  >
18
- {{ state.submitLabel || (state.pending && !state.canSend ? 'Sending…' : 'Send') }}
18
+ {{ state.submitLabel || (state.pending && !state.canSend ? t('jskit.assistant.composer.sending') : t('jskit.assistant.composer.send')) }}
19
19
  </v-btn>
20
20
  </div>
21
21
  <slot name="feedback" />
@@ -24,6 +24,9 @@
24
24
  <script setup>
25
25
  import { ref } from "vue";
26
26
  import { mdiSend, mdiStopCircleOutline } from "@mdi/js";
27
+ import { useJskitText } from "../i18n.js";
28
+
29
+ const { t } = useJskitText();
27
30
  defineProps({ state: { type: Object, required: true } });
28
31
  const emit = defineEmits(["submit", "stop"]);
29
32
  const sendButton = ref(null);
@@ -38,20 +38,20 @@
38
38
  aria-hidden="true"
39
39
  class="assistant-composer-support__loading"
40
40
  >
41
- <span>Thinking of a few ideas</span>
41
+ <span>{{ t("jskit.assistant.composer.thinkingOfIdeas") }}</span>
42
42
  </div>
43
43
 
44
44
  <div
45
45
  v-else
46
46
  class="assistant-composer-support__options"
47
- aria-label="Suggested prompts"
47
+ :aria-label="t('jskit.assistant.composer.suggestedPrompts')"
48
48
  aria-orientation="horizontal"
49
49
  role="group"
50
50
  >
51
51
  <v-btn
52
52
  v-for="suggestion in suggestions"
53
53
  :key="suggestion.prompt"
54
- :aria-label="`Use suggestion: ${suggestion.prompt}`"
54
+ :aria-label="t('jskit.assistant.composer.useSuggestion', { prompt: suggestion.prompt })"
55
55
  class="assistant-composer-support__option"
56
56
  rounded="xl"
57
57
  size="small"
@@ -74,6 +74,9 @@
74
74
  <script setup>
75
75
  import { computed } from "vue";
76
76
  import { mdiLightbulbOnOutline } from "@mdi/js";
77
+ import { useJskitText } from "../i18n.js";
78
+
79
+ const { t } = useJskitText();
77
80
 
78
81
  defineEmits(["dismiss", "focusout", "preview", "select"]);
79
82
  const props = defineProps({
@@ -106,10 +109,10 @@ const statusAnnouncement = computed(() => {
106
109
  return String(props.activity.label || "").trim();
107
110
  }
108
111
  if (mode.value === "loading") {
109
- return "Thinking of a few ideas.";
112
+ return t("jskit.assistant.composer.thinkingOfIdeasStatus");
110
113
  }
111
114
  if (mode.value === "ready") {
112
- return `${props.suggestions.length} suggested prompts are available before the message controls.`;
115
+ return t("jskit.assistant.composer.suggestionsAvailable", { count: props.suggestions.length });
113
116
  }
114
117
  return "";
115
118
  });
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <section ref="container" class="assistant-conversation" :class="{ 'assistant-conversation--error': conversation.error }" :aria-label="label">
2
+ <section ref="container" class="assistant-conversation" :class="{ 'assistant-conversation--error': conversation.error }" :aria-label="label ?? t('jskit.assistant.chat.label')">
3
3
  <section
4
4
  v-if="$slots.avatar" class="assistant-conversation__avatar"
5
- :class="{ 'assistant-conversation__avatar--hidden': avatarSize === 'hidden' }" aria-label="Conversation avatar"
5
+ :class="{ 'assistant-conversation__avatar--hidden': avatarSize === 'hidden' }" :aria-label="t('jskit.assistant.chat.avatar')"
6
6
  >
7
7
  <div v-show="avatarSize === 'hidden'" ref="avatarControls" class="assistant-conversation__avatar-controls">
8
8
  <slot name="avatar-control" :size="avatarSize" :height="avatarHeight" />
@@ -10,7 +10,7 @@
10
10
  :icon="mdiAccountCircleOutline" size="small"
11
11
  variant="text" :width="44" :min-width="44" :min-height="44"
12
12
  class="assistant-conversation__avatar-size"
13
- aria-label="Show avatar" title="Show avatar"
13
+ :aria-label="t('jskit.assistant.chat.showAvatar')" :title="t('jskit.assistant.chat.showAvatar')"
14
14
  :aria-expanded="avatarSize !== 'hidden'" @click="toggleAvatar"
15
15
  >
16
16
  <span class="assistant-conversation__avatar-size-disc">
@@ -27,7 +27,7 @@
27
27
  <v-btn
28
28
  :icon="mdiMinus" size="small" variant="text" color="primary"
29
29
  :width="40" :min-width="40" :min-height="44" class="assistant-conversation__avatar-size"
30
- aria-label="Minimise avatar" title="Minimise avatar" :aria-expanded="true"
30
+ :aria-label="t('jskit.assistant.chat.minimiseAvatar')" :title="t('jskit.assistant.chat.minimiseAvatar')" :aria-expanded="true"
31
31
  @click="toggleAvatar"
32
32
  >
33
33
  <span class="assistant-conversation__avatar-size-disc assistant-conversation__avatar-size-disc--minimise">
@@ -49,13 +49,13 @@
49
49
  <div v-if="scope.turn.preview && scope.turn.previewMessage?.actions" class="assistant-conversation__preview-actions">
50
50
  <v-btn
51
51
  v-if="scope.turn.previewMessage.actions.discard" :icon="mdiClose" size="small" variant="text"
52
- aria-label="Discard unsent message" title="Discard unsent message"
52
+ :aria-label="t('jskit.assistant.chat.discardUnsent')" :title="t('jskit.assistant.chat.discardUnsent')"
53
53
  :disabled="!scope.turn.previewMessage.actions.canDiscard"
54
54
  @click="scope.turn.previewMessage.actions.discard(scope.turn.turnId)"
55
55
  />
56
56
  <v-btn
57
57
  v-if="scope.turn.previewMessage.actions.edit" :icon="mdiPencil" size="small" variant="text"
58
- aria-label="Edit unsent message" title="Edit unsent message"
58
+ :aria-label="t('jskit.assistant.chat.editUnsent')" :title="t('jskit.assistant.chat.editUnsent')"
59
59
  :disabled="!scope.turn.previewMessage.actions.canEdit"
60
60
  @click="scope.turn.previewMessage.actions.edit(scope.turn.turnId)"
61
61
  />
@@ -64,7 +64,7 @@
64
64
  :disabled="!scope.turn.previewMessage.actions.canSend"
65
65
  @click="scope.turn.previewMessage.actions.send(scope.turn.turnId)"
66
66
  >
67
- {{ scope.turn.previewMessage.actions.sending ? 'Sending…' : 'Send' }}
67
+ {{ scope.turn.previewMessage.actions.sending ? t('jskit.assistant.composer.sending') : t('jskit.assistant.composer.send') }}
68
68
  </v-btn>
69
69
  </div>
70
70
  <slot name="user-message-actions" v-bind="scope" />
@@ -88,7 +88,7 @@
88
88
  <AssistantPromptInput
89
89
  v-if="adapter.composer" ref="input" v-bind="adapter.composer" class="assistant-conversation__composer"
90
90
  :model-value="adapter.composer.draft || ''" :submit-enabled="canSend"
91
- :aria-label="adapter.composer.ariaLabel || 'Message AI assistant'" :rows="adapter.composer.rows || 2"
91
+ :aria-label="adapter.composer.ariaLabel || t('jskit.assistant.chat.messageAssistant')" :rows="adapter.composer.rows || 2"
92
92
  :described-by="activity.label || adapter.suggestions?.visible ? statusId : adapter.composer.describedBy"
93
93
  :placeholder="suggestionPreview || adapter.composer.placeholder" :placeholder-affects-height="!suggestionPreview"
94
94
  :attachment-state="attachmentState" :dragging="attachmentsEnabled && adapter.attachments.dragActive"
@@ -125,7 +125,7 @@
125
125
  @select-variant="adapter.models.selectVariant?.($event)" @apply="applyModel" @reload="adapter.models.reload?.()"
126
126
  />
127
127
  <v-btn
128
- v-if="attachmentsEnabled" aria-label="Attach files" title="Attach files" size="small" variant="text" :icon="mdiPaperclip"
128
+ v-if="attachmentsEnabled" :aria-label="t('jskit.assistant.chat.attachFiles')" :title="t('jskit.assistant.chat.attachFiles')" size="small" variant="text" :icon="mdiPaperclip"
129
129
  :disabled="!adapter.attachments.canAddFiles" @click="fileInput?.click()"
130
130
  />
131
131
  <slot
@@ -163,10 +163,13 @@ import AssistantGoalControl from "./AssistantGoalControl.vue";
163
163
  import AssistantAttachmentQueue from "./AssistantAttachmentQueue.vue";
164
164
  import AssistantQuestionInputs from "./AssistantQuestionInputs.vue";
165
165
  import AssistantModelControl from "./AssistantModelControl.vue";
166
+ import { useJskitText } from "../i18n.js";
167
+
168
+ const { t } = useJskitText();
166
169
  const props = defineProps({
167
170
  adapter: { type: Object, required: true },
168
171
  avatarSize: { type: String, default: "compact", validator: (value) => ["hidden", "compact", "standard", "large"].includes(value) },
169
- label: { type: String, default: "Assistant conversation" },
172
+ label: { type: String, default: undefined },
170
173
  configuration: { type: Object, default: () => ({}) },
171
174
  configurationFields: { type: Array, default: () => [] },
172
175
  configurationMode: { type: String, default: "hidden", validator: (value) => ["hidden", "readonly", "editable"].includes(value) }
@@ -298,8 +301,8 @@ const working = computed(() => conversation.value.working ?? (!pending.value &&
298
301
  props.adapter.composer?.canStop || conversation.value.turns?.some(turn => turn.pending)
299
302
  )));
300
303
  const activity = computed(() => props.adapter.activity ?? {
301
- label: props.adapter.composer?.stopPending ? "Stopping…"
302
- : pending.value ? "Sending to assistant…" : working.value ? "Assistant is working…" : ""
304
+ label: props.adapter.composer?.stopPending ? t("jskit.assistant.composer.stopping")
305
+ : pending.value ? t("jskit.assistant.chat.sendingToAssistant") : working.value ? t("jskit.assistant.chat.working") : ""
303
306
  });
304
307
  const suggestionPreview = computed(() => !props.adapter.composer?.draft && props.adapter.suggestions?.visible
305
308
  ? props.adapter.suggestions.preview : "");
@@ -364,7 +367,7 @@ async function submit() {
364
367
  const controller = props.adapter.delivery;
365
368
  if (!controller || !props.adapter.actions?.setDraft) return;
366
369
  const message = String(props.adapter.composer.draft || "").trim() ||
367
- (files.length ? props.adapter.composer.attachmentMessage || "Please review the attached files." : "");
370
+ (files.length ? props.adapter.composer.attachmentMessage || t("jskit.assistant.chat.reviewAttachedFiles") : "");
368
371
  if (!message) return;
369
372
  const payload = {
370
373
  ...props.adapter.composer.payload,
@@ -5,11 +5,11 @@
5
5
  variant="text"
6
6
  color="error"
7
7
  size="x-small"
8
- aria-label="Retry delivery check"
8
+ :aria-label="t('jskit.assistant.delivery.retryCheck')"
9
9
  :disabled="message.delivery.checking"
10
10
  @click="$emit('check-delivery', message.delivery.messageId)"
11
11
  >
12
- {{ message.delivery.checking ? 'Checking…' : 'Retry' }}
12
+ {{ message.delivery.checking ? t('jskit.assistant.delivery.checking') : t('jskit.assistant.delivery.retry') }}
13
13
  </v-btn>
14
14
  </div>
15
15
  <slot v-else>
@@ -19,6 +19,9 @@
19
19
 
20
20
  <script setup>
21
21
  import LongTextPreviewBlocks from "./LongTextPreviewBlocks.vue";
22
+ import { useJskitText } from "../i18n.js";
23
+
24
+ const { t } = useJskitText();
22
25
 
23
26
  defineProps({ message: { type: Object, required: true } });
24
27
  defineEmits(["check-delivery", "link-click"]);
@@ -1,5 +1,12 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, onScopeDispose, ref, watch } from "vue";
3
+ import { useJskitText } from "../i18n.js";
4
+
5
+ const { t } = useJskitText();
6
+ const STATUS_KEYS = Object.freeze({
7
+ active: "jskit.assistant.goal.running", paused: "jskit.assistant.goal.paused", blocked: "jskit.assistant.goal.blocked",
8
+ usageLimited: "jskit.assistant.goal.usageLimited", budgetLimited: "jskit.assistant.goal.budgetLimited", complete: "jskit.assistant.goal.complete"
9
+ });
3
10
 
4
11
  const props = defineProps({ state: { type: Object, default: null } });
5
12
  const open = ref(false);
@@ -9,13 +16,12 @@ const now = ref(Date.now());
9
16
  const goal = computed(() => props.state?.loading || props.state?.available === false ? null : props.state?.goal);
10
17
  const running = computed(() => goal.value?.status === "active");
11
18
  const working = computed(() => running.value && props.state?.working === true);
12
- const label = computed(() => props.state?.loading ? "Loading goal…" : props.state?.available === false ? "Goal unavailable" : ({
13
- active: "Goal running", paused: "Goal paused", blocked: "Goal blocked",
14
- usageLimited: "Goal waiting for allowance", budgetLimited: "Goal budget reached", complete: "Goal complete"
15
- })[goal.value?.status] || "Set goal");
19
+ const label = computed(() => props.state?.loading ? t("jskit.assistant.goal.loading") : props.state?.available === false ? t("jskit.assistant.goal.unavailable")
20
+ : t(Object.hasOwn(STATUS_KEYS, goal.value?.status ?? "") ? STATUS_KEYS[goal.value.status] : "jskit.assistant.goal.set"));
16
21
  const nativeElapsed = computed(() => Number.isFinite(goal.value?.elapsedSeconds));
17
22
  const createdAt = computed(() => typeof goal.value?.createdAt === "string" ? Date.parse(goal.value.createdAt) : NaN);
18
- const elapsedLabel = computed(() => nativeElapsed.value ? "Running time" : "Elapsed since goal started (including pauses)");
23
+ const elapsedLabel = computed(() => nativeElapsed.value ? t("jskit.assistant.goal.runningTime") : t("jskit.assistant.goal.elapsedSinceStart"));
24
+ const elapsedText = computed(() => elapsed.value ? t("jskit.assistant.goal.elapsed", { label: elapsedLabel.value, elapsed: elapsed.value }) : "");
19
25
  const clockRunning = computed(() => nativeElapsed.value ? running.value && Number.isFinite(goal.value?.sampledAt)
20
26
  : Boolean(goal.value && goal.value.status !== "complete" && Number.isFinite(createdAt.value)));
21
27
  const elapsed = computed(() => {
@@ -56,26 +62,26 @@ async function setGoal() {
56
62
  <template>
57
63
  <v-menu v-if="state && state.enabled !== false" v-model="open" location="top" :close-on-content-click="false">
58
64
  <template #activator="{ props: menuProps }">
59
- <v-btn v-bind="menuProps" class="assistant-goal" size="small" variant="text" :aria-label="label" :title="[label, elapsed && `${elapsedLabel}: ${elapsed}`, goal?.objective].filter(Boolean).join('\n')">
65
+ <v-btn v-bind="menuProps" class="assistant-goal" size="small" variant="text" :aria-label="label" :title="[label, elapsedText, goal?.objective].filter(Boolean).join('\n')">
60
66
  <span v-if="goal" class="assistant-goal__light" :class="{ 'assistant-goal__light--running': running, 'assistant-goal__light--working': working, 'assistant-goal__light--paused': goal.status === 'paused' }" aria-hidden="true" />
61
- <span class="assistant-goal__label">{{ state.loading ? 'Loading goal…' : state.available === false ? 'Goal unavailable' : goal ? 'Goal' : 'Set goal' }}</span>
67
+ <span class="assistant-goal__label">{{ state.loading ? t('jskit.assistant.goal.loading') : state.available === false ? t('jskit.assistant.goal.unavailable') : goal ? t('jskit.assistant.goal.goal') : t('jskit.assistant.goal.set') }}</span>
62
68
  <span v-if="elapsed" class="assistant-goal__elapsed" aria-hidden="true">{{ elapsed }}</span>
63
69
  </v-btn>
64
70
  </template>
65
71
  <v-card class="assistant-goal__details pa-3" max-width="360">
66
72
  <strong role="status">{{ label }}</strong>
67
- <v-btn v-if="(running || (state.pauseBlocked && goal?.status === 'blocked')) && state.pause" size="small" :disabled="state.pending" @click="state.pause()">{{ state.action === 'pause' ? 'Pausing…' : 'Pause goal' }}</v-btn>
68
- <v-btn v-if="['paused', 'blocked', 'usageLimited'].includes(goal?.status) && state.resume" size="small" :disabled="state.pending" @click="state.resume()">{{ state.action === 'resume' ? 'Resuming…' : 'Resume goal' }}</v-btn>
69
- <v-btn v-if="goal && goal.status !== 'complete' && state.cancel" color="error" size="small" variant="text" :disabled="state.pending" @click="state.cancel()">{{ state.action === 'cancel' ? 'Cancelling…' : 'Cancel goal' }}</v-btn>
73
+ <v-btn v-if="(running || (state.pauseBlocked && goal?.status === 'blocked')) && state.pause" size="small" :disabled="state.pending" @click="state.pause()">{{ state.action === 'pause' ? t('jskit.assistant.goal.pausing') : t('jskit.assistant.goal.pause') }}</v-btn>
74
+ <v-btn v-if="['paused', 'blocked', 'usageLimited'].includes(goal?.status) && state.resume" size="small" :disabled="state.pending" @click="state.resume()">{{ state.action === 'resume' ? t('jskit.assistant.goal.resuming') : t('jskit.assistant.goal.resume') }}</v-btn>
75
+ <v-btn v-if="goal && goal.status !== 'complete' && state.cancel" color="error" size="small" variant="text" :disabled="state.pending" @click="state.cancel()">{{ state.action === 'cancel' ? t('jskit.assistant.goal.cancelling') : t('jskit.assistant.goal.cancel') }}</v-btn>
70
76
  <p v-if="goal" class="my-2">{{ goal.objective }}</p>
71
77
  <p v-if="goal?.reason" class="text-body-small my-2">{{ goal.reason }}</p>
72
- <p v-if="elapsed" class="text-body-small my-2">{{ elapsedLabel }}: {{ elapsed }}</p>
73
- <p v-if="running || goal?.status === 'paused'" class="text-body-small mt-2">{{ state.pauseInterruptsTurn ? 'Pause stops the current turn and keeps the goal for later.' : 'Pausing prevents further automatic turns. Use Stop to interrupt the current turn.' }}</p>
74
- <p v-if="goal && goal.status !== 'complete' && state.cancel" class="text-body-small mt-2">{{ state.cancelInterruptsTurn ? 'Cancel stops the current turn and clears the goal.' : 'Cancel removes this goal. Use Stop to interrupt a turn already running.' }}</p>
78
+ <p v-if="elapsed" class="text-body-small my-2">{{ elapsedText }}</p>
79
+ <p v-if="running || goal?.status === 'paused'" class="text-body-small mt-2">{{ state.pauseInterruptsTurn ? t('jskit.assistant.goal.pauseInterruptsTurn') : t('jskit.assistant.goal.pausePreventsTurns') }}</p>
80
+ <p v-if="goal && goal.status !== 'complete' && state.cancel" class="text-body-small mt-2">{{ state.cancelInterruptsTurn ? t('jskit.assistant.goal.cancelInterruptsTurn') : t('jskit.assistant.goal.cancelRemovesGoal') }}</p>
75
81
  <form v-if="canSet" class="assistant-goal__form mt-2" @submit.prevent="setGoal">
76
- <v-textarea v-model="objective" label="Goal objective" rows="2" max-rows="4" maxlength="4000" auto-grow :disabled="state.pending" hide-details />
77
- <v-text-field v-if="state.tokenBudgetSupported !== false" v-model="tokenBudget" label="Token budget (optional)" type="number" min="1" step="1" :disabled="state.pending" :error="!validBudget" hide-details />
78
- <v-btn type="submit" size="small" :disabled="state.pending || !objective.trim() || !validBudget">{{ state.action === 'set' ? 'Starting…' : 'Start goal' }}</v-btn>
82
+ <v-textarea v-model="objective" :label="t('jskit.assistant.goal.objective')" rows="2" max-rows="4" maxlength="4000" auto-grow :disabled="state.pending" hide-details />
83
+ <v-text-field v-if="state.tokenBudgetSupported !== false" v-model="tokenBudget" :label="t('jskit.assistant.goal.tokenBudget')" type="number" min="1" step="1" :disabled="state.pending" :error="!validBudget" hide-details />
84
+ <v-btn type="submit" size="small" :disabled="state.pending || !objective.trim() || !validBudget">{{ state.action === 'set' ? t('jskit.assistant.goal.starting') : t('jskit.assistant.goal.start') }}</v-btn>
79
85
  </form>
80
86
  <p v-if="state.error" role="alert" class="text-error text-body-small mt-2">{{ state.error }}</p>
81
87
  <slot :goal="goal" />
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <ul
3
3
  v-if="attachments.length"
4
- aria-label="Attached files"
4
+ :aria-label="t('jskit.assistant.attachments.attached')"
5
5
  class="assistant-conversation-attachments"
6
6
  >
7
7
  <li
@@ -29,10 +29,10 @@
29
29
  {{ attachment.reference }} {{ attachment.fileName }}
30
30
  </span>
31
31
  <span
32
- v-if="attachmentSizeLabel(attachment.size)"
32
+ v-if="attachmentSizeText(attachment.size)"
33
33
  class="assistant-conversation-attachments__size text-label-small"
34
34
  >
35
- {{ attachmentSizeLabel(attachment.size) }}
35
+ {{ attachmentSizeText(attachment.size) }}
36
36
  </span>
37
37
  </span>
38
38
  </component>
@@ -42,7 +42,10 @@
42
42
 
43
43
  <script setup>
44
44
  import { mdiFileOutline, mdiImageOutline } from "@mdi/js";
45
- import { attachmentSizeLabel } from "../../shared/conversation/attachments.js";
45
+ import { useJskitText } from "../i18n.js";
46
+ import { attachmentSizeText } from "./attachmentSize.js";
47
+
48
+ const { t } = useJskitText();
46
49
 
47
50
  defineProps({
48
51
  attachments: { type: Array, default: () => [] },