@jskit-ai/assistant-core 0.1.243 → 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 (44) 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/openCodeProcess.js +12 -3
  38. package/src/server/conversation/runtime.js +193 -141
  39. package/src/server/lib/ndjson.js +19 -1
  40. package/src/server/lib/providers/common.js +7 -2
  41. package/src/shared/conversation/numberedQuestionSugar.js +7 -1
  42. package/src/shared/conversation/turns.js +10 -2
  43. package/test/i18nCatalog.test.js +22 -0
  44. package/test/openCodeProcess.test.js +15 -1
@@ -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
+ });
@@ -3,7 +3,7 @@ import { access, mkdtemp, rm } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
- import { createOpenCodeServerProcess } from "../src/server/conversation/openCodeProcess.js";
6
+ import { createOpenCodeServerProcess, openCodeProcessEnvironment } from "../src/server/conversation/openCodeProcess.js";
7
7
 
8
8
  async function fixture(t, options = {}) {
9
9
  const workdir = await mkdtemp(path.join(os.tmpdir(), "jskit-opencode-process-"));
@@ -175,3 +175,17 @@ test("production stop retains failure for retry and keeps the complete confirmed
175
175
  assert.equal(attempt, 3);
176
176
  await assert.rejects(access(f.config.privateRoot), { code: "ENOENT" });
177
177
  });
178
+
179
+ test("a server that needs no packages makes OpenCode's plugin install fail at once", () => {
180
+ const installing = openCodeProcessEnvironment({}, { cacheRoot: "/cache" });
181
+ assert.equal(installing.npm_config_cache, "/cache/npm");
182
+ assert.equal(installing.npm_config_prefer_offline, "true");
183
+ assert.equal(installing.npm_config_offline, undefined);
184
+ // A real OpenCode 1.18.31 catalogue server stopped mid-install ignored SIGTERM
185
+ // for the whole systemd stop timeout; offline from an empty cache it stopped in
186
+ // under 50 ms and returned the same providers and agents.
187
+ const offline = openCodeProcessEnvironment({}, { cacheRoot: "/cache", packageInstalls: false });
188
+ assert.equal(offline.npm_config_offline, "true");
189
+ assert.equal(offline.npm_config_cache, "/cache/npm-offline");
190
+ assert.equal(offline.npm_config_prefer_offline, undefined);
191
+ });