@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
@@ -27,6 +27,23 @@ function endNdjson(reply) {
27
27
  reply.raw.end();
28
28
  }
29
29
 
30
+ function isRecord(value) {
31
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
32
+ }
33
+
34
+ // The client renders code, params or messageCode in the user's language and
35
+ // falls back to message, like an HTTP error response.
36
+ function streamMessageFields(error, hidden) {
37
+ if (hidden) {
38
+ return { params: {}, messageCode: "assistant.stream.failed", messageParams: {} };
39
+ }
40
+ const params = isRecord(error?.params) ? error.params : {};
41
+ if (error?.messageCode) {
42
+ return { params, messageCode: String(error.messageCode), messageParams: isRecord(error.messageParams) ? error.messageParams : {} };
43
+ }
44
+ return error?.message ? { params } : { params, messageCode: "assistant.stream.requestFailed", messageParams: {} };
45
+ }
46
+
30
47
  function mapStreamError(error) {
31
48
  const status = Number(error?.status || error?.statusCode || 500);
32
49
  const safeStatus = Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
@@ -34,7 +51,8 @@ function mapStreamError(error) {
34
51
  return Object.freeze({
35
52
  code: String(error?.code || "assistant_stream_failed").trim() || "assistant_stream_failed",
36
53
  message: safeStatus >= 500 ? "Assistant stream failed." : String(error?.message || "Request failed."),
37
- status: safeStatus
54
+ status: safeStatus,
55
+ ...streamMessageFields(error, safeStatus >= 500)
38
56
  });
39
57
  }
40
58
 
@@ -58,7 +58,7 @@ function normalizeOptionalHttpUrl(value, { context = "assistant baseUrl" } = {})
58
58
 
59
59
  function createDisabledClient({ provider = DEFAULT_AI_PROVIDER, model = "" } = {}) {
60
60
  const disabledError = () => {
61
- throw new AppError(503, "Assistant provider is not configured.");
61
+ throw new AppError(503, "Assistant provider is not configured.", { code: "assistant.provider.notConfigured" });
62
62
  };
63
63
 
64
64
  return Object.freeze({
@@ -80,9 +80,14 @@ function createProviderRequestError({ status = 500, code = "assistant_provider_f
80
80
  const normalizedCode = normalizeText(code) || "assistant_provider_failed";
81
81
  const normalizedMessage =
82
82
  normalizeText(message) || (safeStatus >= 500 ? "Assistant provider request failed." : "Request failed.");
83
+ // A provider's own message is shown as sent; JSKIT's fallback sentences are named.
84
+ const fallbackMessage = normalizeText(message) ? {} : safeStatus >= 500
85
+ ? { messageCode: "assistant.provider.requestFailed" }
86
+ : { messageCode: "assistant.provider.requestRejected" };
83
87
 
84
88
  return new AppError(safeStatus, normalizedMessage, {
85
- code: normalizedCode
89
+ code: normalizedCode,
90
+ ...fallbackMessage
86
91
  });
87
92
  }
88
93
 
@@ -264,6 +264,10 @@ function numberedQuestionSugarForMessageInput({
264
264
  return parseNumberedQuestionPrompt(message);
265
265
  }
266
266
 
267
+ // requiredMessage stays English; a client shows it in the active language with
268
+ // serverMessage({ message: requiredMessage, messageCode: requiredMessageCode, messageParams: requiredMessageParams }).
269
+ const ANSWER_REQUIRED = Object.freeze({ messageCode: "assistant.questions.answerRequired" });
270
+
267
271
  function numberedQuestionInputFields(questions = [], options = {}) {
268
272
  const autocomplete = String(options?.autocomplete || "").trim();
269
273
  const density = String(options?.density || "").trim();
@@ -274,7 +278,9 @@ function numberedQuestionInputFields(questions = [], options = {}) {
274
278
  label: question.label,
275
279
  name: question.name,
276
280
  required: true,
277
- requiredMessage: `Answer question ${question.number}.`
281
+ requiredMessage: `Answer question ${question.number}.`,
282
+ requiredMessageCode: ANSWER_REQUIRED.messageCode,
283
+ requiredMessageParams: { number: question.number }
278
284
  }));
279
285
  }
280
286
 
@@ -9,6 +9,13 @@ export function conversationTurnMessages(turn) {
9
9
  ].filter(Boolean);
10
10
  }
11
11
 
12
+ // A finished turn without text shows its status. The English stays in text; textCode
13
+ // lets a client show it in the user's language.
14
+ const STATUS_TEXTS = Object.freeze({
15
+ interrupted: Object.freeze({ text: "Stopped.", messageCode: "assistant.turn.stopped" }),
16
+ failed: Object.freeze({ text: "Assistant stopped with an error.", messageCode: "assistant.turn.failed" })
17
+ });
18
+
12
19
  /** Adapt ordered messages to the transcript's turn model without changing their metadata. */
13
20
  export function conversationTurnsFromMessages(messages = []) {
14
21
  const turns = [];
@@ -30,8 +37,9 @@ export function conversationTurnsFromMessages(messages = []) {
30
37
  for (const [progressIndex, progress] of (message.progressUpdates || []).entries()) {
31
38
  current.messages.push({ ...progress, role: progress.role || "thinking", messageId: `${message.messageId}:progress:${progress.id || progressIndex}` });
32
39
  }
33
- if (!message.text && ["failed", "interrupted"].includes(message.status)) {
34
- message.text = message.status === "interrupted" ? "Stopped." : "Assistant stopped with an error.";
40
+ if (!message.text && Object.hasOwn(STATUS_TEXTS, message.status)) {
41
+ message.text = STATUS_TEXTS[message.status].text;
42
+ message.textCode = STATUS_TEXTS[message.status].messageCode;
35
43
  }
36
44
  current.assistant = message;
37
45
  }
@@ -0,0 +1,22 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { evaluateI18nCatalogContract } from "../../../tooling/test-support/i18nCatalogContract.mjs";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
+
10
+ test("assistant-core English catalog matches its source", () => {
11
+ const { problems } = evaluateI18nCatalogContract({
12
+ repoRoot: REPO_ROOT,
13
+ packageDir: path.join(REPO_ROOT, "packages", "assistant-core"),
14
+ // runOpenCodeConversationTurn builds these codes as `assistant_opencode_turn_${reason}`.
15
+ externalCodes: [
16
+ "assistant_opencode_turn_thread_mismatch",
17
+ "assistant_opencode_turn_fresh_thread_required",
18
+ "assistant_opencode_turn_unrelated_history"
19
+ ]
20
+ });
21
+ assert.deepEqual(problems, []);
22
+ });