@jskit-ai/assistant-core 0.2.24 → 0.2.26
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 +6 -1
- package/examples/conversation/package.json +6 -6
- package/package.json +8 -8
- package/src/client/components/AssistantSettingsFormCard.vue +14 -9
- package/src/client/conversation/AssistantAttachmentQueue.vue +2 -4
- package/src/client/conversation/AssistantConversationElement.vue +2 -2
- package/src/client/conversation/AssistantMessageAttachments.vue +2 -2
- package/src/client/conversation/AssistantModelControl.vue +5 -4
- package/src/client/conversation/AssistantPromptInput.vue +24 -18
- package/src/client/conversation/AssistantTranscript.css +7 -15
- package/src/client/conversation/LongTextInlineParts.vue +2 -3
- package/src/client/conversation/LongTextPreviewBlocks.vue +7 -6
- package/src/server/conversation/codexProviderOwner.js +6 -1
- package/src/server/conversation/conversationEvents.js +30 -2
- package/src/server/conversation/conversationHandles.js +1 -1
- package/src/server/lib/providers/openAiCompatibleClient.js +12 -8
- package/test/assistantScroll.browser.test.js +53 -103
- package/test/claudeConversationSession.test.js +16 -1
- package/test/componentContracts.test.js +44 -0
- package/test/conversationAvatarThumbnail.test.js +3 -4
- package/test/conversationSession.test.js +63 -0
- package/test/fixtures/nestedListQuestionMessage.md +19 -19
- package/test/longTextBlocks.test.js +14 -14
- package/test/messageDelivery.browser.test.js +12 -14
- package/test/openAiLoading.test.js +25 -0
- package/test/opencodeConversationSession.test.js +20 -4
- package/test/transcriptTheme.browser.test.js +6 -6
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
Here's how I'd run a final
|
|
1
|
+
Here's how I'd run a final check of the garden planner in three steps: I do a careful pass on my own first, then a short session with you for the parts that need you, then I tidy up.
|
|
2
2
|
|
|
3
|
-
**Step 1: I
|
|
4
|
-
- **
|
|
5
|
-
- **Gather the "
|
|
6
|
-
- **I can
|
|
7
|
-
- **Needs you:** real accounts,
|
|
8
|
-
- **Can't be
|
|
9
|
-
- **Output:** one gap list, saved to
|
|
3
|
+
**Step 1: I check on my own (no need for you)**
|
|
4
|
+
- **Try every screen.** I open every screen and compare it against the design notes: every button, the + menu, every right-click menu (beds, plants, seasons, saved plans), every keyboard shortcut on the help sheet, and every settings row. Anything with no note is a gap.
|
|
5
|
+
- **Gather the "open question" lists.** Every design note has one, and I'll merge them all into a single gap list. Each item gets labelled as one of three kinds:
|
|
6
|
+
- **I can check it now:** for example, renaming and deleting beds, or a plan made from a template.
|
|
7
|
+
- **Needs you:** real accounts, printing, a second account, payment, your tablet, links that open in a browser.
|
|
8
|
+
- **Can't be checked at all.**
|
|
9
|
+
- **Output:** one gap list, saved to the shared folder, with every item marked done, checkable, needs you, or impossible.
|
|
10
10
|
|
|
11
11
|
**Step 2: a session with you, for the "needs you" items and things only you know about**
|
|
12
|
-
- **You
|
|
13
|
-
- **I
|
|
14
|
-
- **Things outside the
|
|
12
|
+
- **You use the planner on your own computer**, as you normally would: your knowledge of features, the test account, the tablet app, links opening in your browser.
|
|
13
|
+
- **I take notes as you go** and capture screenshots when you tell me something interesting is happening. Just tell me what you did in plain words ("now I'm printing a plan", "this is the share link in my browser").
|
|
14
|
+
- **Things outside the planner itself:** put screenshots in the shared folder, for example from the tablet app, the help pages, or emails it sends. I'll file them.
|
|
15
15
|
- **What I need from you:**
|
|
16
16
|
- which features you know or suspect exist that I haven't covered (I'll ask about specific suspects from my list)
|
|
17
|
-
- the test
|
|
17
|
+
- the test account, so I can see the real signed-in states
|
|
18
18
|
- your call on anything that sends real data or costs money
|
|
19
19
|
|
|
20
20
|
**Step 3: I tidy up**
|
|
21
|
-
- **For
|
|
22
|
-
- Newly
|
|
23
|
-
- Each one gets a
|
|
21
|
+
- **For the design notes:**
|
|
22
|
+
- Newly found screens go into the notes as additions to the screen map.
|
|
23
|
+
- Each one gets a short note, written when you or the team ask for it. The release plan allows that.
|
|
24
24
|
- **For you:** a final coverage summary.
|
|
25
25
|
|
|
26
|
-
**One thing to keep in mind:**
|
|
26
|
+
**One thing to keep in mind:** some screens still show sample data. During this phase, treat anything odd there as a question for me rather than a bug.
|
|
27
27
|
|
|
28
|
-
[1] Shall I start Step 1 (the solo
|
|
28
|
+
[1] Shall I start Step 1 (the solo check and gap list) now?
|
|
29
29
|
Possible answers:
|
|
30
30
|
- Yes, start now
|
|
31
31
|
- Wait until I'm ready to do Step 2 as well
|
|
32
32
|
|
|
33
33
|
[2] For Step 2, how would you like to work?
|
|
34
34
|
Possible answers:
|
|
35
|
-
- I
|
|
36
|
-
- I'll put screenshots in the
|
|
35
|
+
- I use the planner while you take notes
|
|
36
|
+
- I'll put screenshots in the shared folder
|
|
37
37
|
- A mix of both
|
|
@@ -22,34 +22,34 @@ function outline(blocks = []) {
|
|
|
22
22
|
test("a question message keeps its intro paragraphs and nested lists apart from the question cards", () => {
|
|
23
23
|
const prompt = parseNumberedQuestionPrompt(message);
|
|
24
24
|
assert.deepEqual(prompt.questions.map((question) => [question.label, question.choices.length]), [
|
|
25
|
-
["Shall I start Step 1 (the solo
|
|
25
|
+
["Shall I start Step 1 (the solo check and gap list) now?", 2],
|
|
26
26
|
["For Step 2, how would you like to work?", 3]
|
|
27
27
|
]);
|
|
28
28
|
assert.equal(prompt.outro, "");
|
|
29
29
|
|
|
30
30
|
assert.deepEqual(outline(parseLongTextReviewBlocks(prompt.intro)), [
|
|
31
|
-
"paragraph: Here's how I'd run a final
|
|
32
|
-
"paragraph: **Step 1: I
|
|
31
|
+
"paragraph: Here's how I'd run a final check of the ",
|
|
32
|
+
"paragraph: **Step 1: I check on my own (no need for",
|
|
33
33
|
{
|
|
34
34
|
ul: [
|
|
35
|
-
"**
|
|
35
|
+
"**Try every screen.** I open every scree",
|
|
36
36
|
{
|
|
37
|
-
"**Gather the \"
|
|
38
|
-
{ ul: ["**I can
|
|
37
|
+
"**Gather the \"open question\" lists.** Ev": [
|
|
38
|
+
{ ul: ["**I can check it now:** for example, ren", "**Needs you:** real accounts, printing, ", "**Can't be checked at all.**"] }
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
|
-
"**Output:** one gap list, saved to
|
|
41
|
+
"**Output:** one gap list, saved to the s"
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
"paragraph: **Step 2: a session with you, for the \"n",
|
|
45
45
|
{
|
|
46
46
|
ul: [
|
|
47
|
-
"**You
|
|
48
|
-
"**I
|
|
49
|
-
"**Things outside the
|
|
47
|
+
"**You use the planner on your own comput",
|
|
48
|
+
"**I take notes as you go** and capture s",
|
|
49
|
+
"**Things outside the planner itself:** p",
|
|
50
50
|
{
|
|
51
51
|
"**What I need from you:**": [
|
|
52
|
-
{ ul: ["which features you know or suspect exist", "the test
|
|
52
|
+
{ ul: ["which features you know or suspect exist", "the test account, so I can see the real ", "your call on anything that sends real da"] }
|
|
53
53
|
]
|
|
54
54
|
}
|
|
55
55
|
]
|
|
@@ -58,14 +58,14 @@ test("a question message keeps its intro paragraphs and nested lists apart from
|
|
|
58
58
|
{
|
|
59
59
|
ul: [
|
|
60
60
|
{
|
|
61
|
-
"**For
|
|
62
|
-
{ ul: ["Newly
|
|
61
|
+
"**For the design notes:**": [
|
|
62
|
+
{ ul: ["Newly found screens go into the notes as", "Each one gets a short note, written when"] }
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
65
|
"**For you:** a final coverage summary."
|
|
66
66
|
]
|
|
67
67
|
},
|
|
68
|
-
"paragraph: **One thing to keep in mind:**
|
|
68
|
+
"paragraph: **One thing to keep in mind:** some scre"
|
|
69
69
|
]);
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { expect } from "../../../tooling/testUtils/browserExpect.mjs";
|
|
5
|
+
import { sharedBrowserFixture } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
6
|
+
|
|
7
|
+
const openFixture = sharedBrowserFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/responsive-assistant", import.meta.url)) });
|
|
6
8
|
|
|
7
9
|
test("the shared element renders delayed sends and supplies retry, edit and cancel without application handlers", {
|
|
8
10
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
9
|
-
}, async () => {
|
|
10
|
-
const vite = await
|
|
11
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
11
|
+
}, async (t) => {
|
|
12
|
+
const { vite, browser } = await openFixture(t);
|
|
12
13
|
try {
|
|
13
14
|
for (const width of [390, 800, 1365]) {
|
|
14
15
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -102,16 +103,14 @@ test("the shared element renders delayed sends and supplies retry, edit and canc
|
|
|
102
103
|
}
|
|
103
104
|
} finally {
|
|
104
105
|
await browser.close();
|
|
105
|
-
await stopProcess(vite);
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
test("chronological messages and Cancel during a delivery check work on phone and desktop", {
|
|
111
111
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
112
|
-
}, async () => {
|
|
113
|
-
const vite = await
|
|
114
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
112
|
+
}, async (t) => {
|
|
113
|
+
const { vite, browser } = await openFixture(t);
|
|
115
114
|
try {
|
|
116
115
|
for (const width of [390, 1365]) {
|
|
117
116
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -148,14 +147,13 @@ test("chronological messages and Cancel during a delivery check work on phone an
|
|
|
148
147
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
149
148
|
await page.close();
|
|
150
149
|
}
|
|
151
|
-
} finally { await browser.close();
|
|
150
|
+
} finally { await browser.close(); }
|
|
152
151
|
});
|
|
153
152
|
|
|
154
153
|
test("accepted local messages precede later replies before their history page arrives", {
|
|
155
154
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
156
|
-
}, async () => {
|
|
157
|
-
const vite = await
|
|
158
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
155
|
+
}, async (t) => {
|
|
156
|
+
const { vite, browser } = await openFixture(t);
|
|
159
157
|
try {
|
|
160
158
|
for (const width of [390, 1365]) {
|
|
161
159
|
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -178,5 +176,5 @@ test("accepted local messages precede later replies before their history page ar
|
|
|
178
176
|
assert.deepEqual(errors, []);
|
|
179
177
|
await page.close();
|
|
180
178
|
}
|
|
181
|
-
} finally { await browser.close();
|
|
179
|
+
} finally { await browser.close(); }
|
|
182
180
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { registerHooks } from "node:module";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
// Every module this process loads from here on; assistant-core is imported after the hook.
|
|
6
|
+
const loadedUrls = [];
|
|
7
|
+
registerHooks({
|
|
8
|
+
load(url, context, nextLoad) {
|
|
9
|
+
loadedUrls.push(url);
|
|
10
|
+
return nextLoad(url, context);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const openAiSdkModules = () => loadedUrls.filter((url) => /\/node_modules\/openai\//u.test(url));
|
|
14
|
+
|
|
15
|
+
test("the OpenAI SDK loads with an OpenAI-compatible client's first completion, not with the server", async () => {
|
|
16
|
+
await import("../src/server/index.js");
|
|
17
|
+
const { createOpenAiCompatibleClient } = await import("../src/server/lib/providers/openAiCompatibleClient.js");
|
|
18
|
+
const client = createOpenAiCompatibleClient({ apiKey: "test-only-key", baseUrl: "http://127.0.0.1:9/v1", model: "gpt-test" });
|
|
19
|
+
assert.equal(client.enabled, true);
|
|
20
|
+
assert.deepEqual(openAiSdkModules(), []);
|
|
21
|
+
|
|
22
|
+
const aborted = AbortSignal.abort();
|
|
23
|
+
await assert.rejects(client.createChatCompletion({ messages: [{ role: "user", content: "Hello" }], signal: aborted }));
|
|
24
|
+
assert.ok(openAiSdkModules().length > 0);
|
|
25
|
+
});
|
|
@@ -77,6 +77,7 @@ async function fixture(t, options = {}) {
|
|
|
77
77
|
const directory = await mkdtemp(path.join(tmpdir(), "jskit-opencode-conversation-"));
|
|
78
78
|
const command = path.join(directory, "opencode.mjs");
|
|
79
79
|
const trace = path.join(directory, "trace.jsonl");
|
|
80
|
+
const partialSeen = path.join(directory, "partial-seen");
|
|
80
81
|
await writeFile(command, `#!${process.execPath}
|
|
81
82
|
import { createServer } from "node:http";
|
|
82
83
|
import { appendFileSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
@@ -90,6 +91,7 @@ async function fixture(t, options = {}) {
|
|
|
90
91
|
const streams = new Set();
|
|
91
92
|
const assistantResponses = ${JSON.stringify(options.assistantResponses || [])};
|
|
92
93
|
const busy = new Set();
|
|
94
|
+
const heldAnswers = new Map();
|
|
93
95
|
let sequence = [...sessions.values()].reduce((sum, session) => sum + session.messages.length, 0);
|
|
94
96
|
const row = (role, id, parts, extra = {}) => ({ info: { role, id, time: { created: ++sequence }, ...extra }, parts });
|
|
95
97
|
const server = createServer(async (req, res) => {
|
|
@@ -132,6 +134,12 @@ async function fixture(t, options = {}) {
|
|
|
132
134
|
}
|
|
133
135
|
if (req.url.includes("/message")) {
|
|
134
136
|
if (${Boolean(options.historyUnavailable)}) return reply({ message: "Native history unavailable" }, 503);
|
|
137
|
+
// A held answer completes at the first read after the test saw its partial streamed, or after 10 s, so a
|
|
138
|
+
// partial that never streams fails the test instead of hanging it.
|
|
139
|
+
const held = heldAnswers.get(session.id);
|
|
140
|
+
if (held && (existsSync(${JSON.stringify(partialSeen)}) || Date.now() - held.since > 10_000)) {
|
|
141
|
+
heldAnswers.delete(session.id); held.complete();
|
|
142
|
+
}
|
|
135
143
|
return reply(session.messages);
|
|
136
144
|
}
|
|
137
145
|
if (req.url.endsWith("/prompt_async")) {
|
|
@@ -217,11 +225,13 @@ async function fixture(t, options = {}) {
|
|
|
217
225
|
log({ system: output.system, sessionId: session.id });
|
|
218
226
|
const answer = row("assistant", randomUUID(), [{ type: "reasoning", text: "Reasoning summary" }, { type: "text", text: "Answer: " }]);
|
|
219
227
|
session.messages.push(answer); save();
|
|
220
|
-
|
|
228
|
+
const complete = () => {
|
|
221
229
|
answer.parts[1].text = answerText;
|
|
222
230
|
answer.info.finish = "stop"; answer.info.time.completed = Date.now(); save();
|
|
223
231
|
busy.delete(session.id);
|
|
224
|
-
}
|
|
232
|
+
};
|
|
233
|
+
if (${Boolean(options.holdPartialAnswer)}) heldAnswers.set(session.id, { complete, since: Date.now() });
|
|
234
|
+
else setTimeout(complete, 100);
|
|
225
235
|
return;
|
|
226
236
|
}
|
|
227
237
|
reply({ message: "Unexpected route" }, 404);
|
|
@@ -259,6 +269,7 @@ async function fixture(t, options = {}) {
|
|
|
259
269
|
limits: { admissionTimeoutMs: 250, ...options.limits }
|
|
260
270
|
},
|
|
261
271
|
binding: () => storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding),
|
|
272
|
+
releasePartialAnswer: () => writeFile(partialSeen, ""),
|
|
262
273
|
async trace() { return (await readFile(trace, "utf8")).trim().split("\n").map(line => JSON.parse(line)); } };
|
|
263
274
|
}
|
|
264
275
|
|
|
@@ -321,9 +332,14 @@ test("OpenCode structured output counts its schema framing before native dispatc
|
|
|
321
332
|
});
|
|
322
333
|
|
|
323
334
|
test("OpenCode admits once, streams and saves native reasoning through the common runtime", async t => {
|
|
324
|
-
|
|
335
|
+
// OpenCode text streams only through the runtime's history reads, so the native answer stays partial until this
|
|
336
|
+
// subscriber has seen it streamed, instead of for a fixed 100 ms that a read under load could miss.
|
|
337
|
+
const f = await fixture(t, { holdPartialAnswer: true });
|
|
325
338
|
const events = [];
|
|
326
|
-
await f.conversation.subscribe(event =>
|
|
339
|
+
await f.conversation.subscribe(event => {
|
|
340
|
+
events.push(event);
|
|
341
|
+
if (event.type === "message" && event.text === "Answer: ") void f.releasePartialAnswer();
|
|
342
|
+
});
|
|
327
343
|
assert.equal((await f.conversation.send(input)).status, "accepted");
|
|
328
344
|
const state = await f.conversation.wait();
|
|
329
345
|
assert.equal(state.conversationLog[0].metadata.runtime.status, "complete");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { expect } from "@playwright/test";
|
|
5
|
+
import { sharedBrowserFixture } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
6
6
|
|
|
7
7
|
// An element's text colour, the theme values it should come from, and its WCAG contrast over what is painted under
|
|
8
8
|
// it: each translucent background is laid over the next one out, down to the first opaque one (white when none is).
|
|
@@ -43,11 +43,12 @@ function readText(element) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const openFixture = sharedBrowserFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/responsive-assistant", import.meta.url)) });
|
|
47
|
+
|
|
46
48
|
test("the task transcript's user bubble, header time and footer follow the light and dark themes", {
|
|
47
49
|
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
48
|
-
}, async () => {
|
|
49
|
-
const vite = await
|
|
50
|
-
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
50
|
+
}, async (t) => {
|
|
51
|
+
const { vite, browser } = await openFixture(t);
|
|
51
52
|
try {
|
|
52
53
|
for (const theme of ["light", "dark"]) {
|
|
53
54
|
const page = await browser.newPage({ viewport: { width: 800, height: 900 } });
|
|
@@ -84,6 +85,5 @@ test("the task transcript's user bubble, header time and footer follow the light
|
|
|
84
85
|
}
|
|
85
86
|
} finally {
|
|
86
87
|
await browser.close();
|
|
87
|
-
await stopProcess(vite);
|
|
88
88
|
}
|
|
89
89
|
});
|