@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.
- package/CHANGELOG.md +539 -0
- package/examples/conversation/package.json +5 -5
- package/locales/en.json +383 -0
- package/package.json +7 -6
- package/src/client/components/AssistantSettingsFormCard.vue +4 -1
- package/src/client/conversation/AssistantAttachmentPreview.vue +6 -3
- package/src/client/conversation/AssistantAttachmentQueue.vue +36 -37
- package/src/client/conversation/AssistantComposerActions.vue +6 -3
- package/src/client/conversation/AssistantComposerSupport.vue +8 -5
- package/src/client/conversation/AssistantConversationElement.vue +16 -13
- package/src/client/conversation/AssistantConversationStatus.vue +5 -2
- package/src/client/conversation/AssistantGoalControl.vue +22 -16
- package/src/client/conversation/AssistantMessageAttachments.vue +7 -4
- package/src/client/conversation/AssistantModelControl.vue +23 -20
- package/src/client/conversation/AssistantProgress.vue +6 -3
- package/src/client/conversation/AssistantQuestionInputs.vue +9 -6
- package/src/client/conversation/AssistantTranscript.vue +39 -28
- package/src/client/conversation/LongTextPreviewBlocks.vue +10 -2
- package/src/client/conversation/attachmentSize.js +38 -0
- package/src/client/conversation/messageDelivery.js +14 -5
- package/src/client/conversation/submitText.js +11 -4
- package/src/client/conversation/useAssistantAttachments.js +40 -30
- package/src/client/conversation/useAssistantSuggestions.js +4 -1
- package/src/client/i18n.js +6 -0
- package/src/client/lib/assistantApi.js +14 -2
- package/src/server/conversation/attachments.js +24 -14
- package/src/server/conversation/claudeProcess.js +4 -2
- package/src/server/conversation/claudeTurn.js +14 -11
- package/src/server/conversation/codexConversations.js +4 -0
- package/src/server/conversation/codexGoalCommands.js +9 -9
- package/src/server/conversation/codexHelperExecution.js +4 -0
- package/src/server/conversation/codexHelperThreads.js +2 -0
- package/src/server/conversation/codexMessageCommands.js +7 -1
- package/src/server/conversation/codexTurnControl.js +2 -1
- package/src/server/conversation/codexTurnState.js +2 -0
- package/src/server/conversation/continuity.js +16 -6
- package/src/server/conversation/openCodeProcess.js +12 -3
- package/src/server/conversation/runtime.js +193 -141
- package/src/server/lib/ndjson.js +19 -1
- package/src/server/lib/providers/common.js +7 -2
- package/src/shared/conversation/numberedQuestionSugar.js +7 -1
- package/src/shared/conversation/turns.js +10 -2
- package/test/i18nCatalog.test.js +22 -0
- package/test/openCodeProcess.test.js +15 -1
package/src/server/lib/ndjson.js
CHANGED
|
@@ -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 &&
|
|
34
|
-
message.text = message.status
|
|
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
|
+
});
|