@jskit-ai/assistant-runtime 0.1.163 → 0.1.164

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.
@@ -18,10 +18,11 @@ function switchWorkspace() { router.push({ query: { workspace: workspace.value =
18
18
  <template>
19
19
  <v-app>
20
20
  <v-main>
21
- <div class="fixture">
21
+ <div class="fixture" :class="{ 'fixture--page': route.query.page === '1' }">
22
22
  <nav aria-label="Fixture controls">
23
23
  <button @click="switchWorkspace">Switch workspace</button>
24
24
  <button @click="mergeContext({ user: { id: context.user.id === '1' ? '2' : '1' } })">Switch user</button>
25
+ <button @click="mergeContext({ workspace: { id: '42', slug: workspace, name: 'Workspace metadata loaded' } })">Load workspace metadata</button>
25
26
  <button @click="surface = surface === 'admin' ? 'console' : 'admin'">Switch surface</button>
26
27
  <button @click="mounted = !mounted">Toggle view</button>
27
28
  <button @click="second = !second">Second instance</button>
@@ -44,4 +45,6 @@ html, body, #app { height: 100%; margin: 0; }
44
45
  .fixture__panes { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; }
45
46
  .fixture__panes > * { flex: 1 1 0; min-width: 0; }
46
47
  .fixture__panes--compact { width: min(360px, 100%); }
48
+ .fixture--page { height: auto; }
49
+ .fixture--page .fixture__panes { display: block; }
47
50
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-runtime",
3
- "version": "0.1.163",
3
+ "version": "0.1.164",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -11,15 +11,15 @@
11
11
  "./server/actionIds": "./src/server/actionIds.js"
12
12
  },
13
13
  "dependencies": {
14
- "@jskit-ai/assistant-core": "0.1.170",
15
- "@jskit-ai/database-runtime": "0.1.192",
14
+ "@jskit-ai/assistant-core": "0.1.171",
15
+ "@jskit-ai/database-runtime": "0.1.193",
16
16
  "json-rest-schema": "^1.0.17"
17
17
  },
18
18
  "peerDependencies": {
19
- "@jskit-ai/http-runtime": "0.1.190",
20
- "@jskit-ai/http-web": "0.1.37",
21
- "@jskit-ai/kernel": "0.1.192",
22
- "@jskit-ai/shell-web": "0.1.196",
19
+ "@jskit-ai/http-runtime": "0.1.191",
20
+ "@jskit-ai/http-web": "0.1.38",
21
+ "@jskit-ai/kernel": "0.1.193",
22
+ "@jskit-ai/shell-web": "0.1.197",
23
23
  "@tanstack/vue-query": "^5.90.5",
24
24
  "vue": "^3.5.13",
25
25
  "vuetify": "^4.0.0"
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, reactive, ref, toRef } from "vue";
2
+ import { computed, onMounted, onScopeDispose, reactive, ref, toRef, watchPostEffect } from "vue";
3
3
  import { AssistantConversationElement } from "@jskit-ai/assistant-core/client/conversation";
4
4
  import { useAssistantRuntime } from "../composables/useAssistantRuntime.js";
5
5
  import { mapAssistantConversationTurns } from "../support/assistantRuntimeState.js";
@@ -15,7 +15,22 @@ const props = defineProps({
15
15
  const runtime = useAssistantRuntime({ surfaceId: toRef(props, "surfaceId") });
16
16
  const state = reactive(runtime.state);
17
17
  const conversation = ref(null);
18
+ const root = ref(null);
18
19
  const historyOpen = ref(false);
20
+ // Page mode owns viewport sizing; compact mode fills the host's bounded pane.
21
+ onMounted(() => {
22
+ const syncPageOffset = () => {
23
+ if (props.layout === "page") root.value.style.setProperty("--assistant-page-top", `${Math.max(0, root.value.getBoundingClientRect().top)}px`);
24
+ };
25
+ const observer = new ResizeObserver(syncPageOffset);
26
+ observer.observe(root.value.parentElement);
27
+ window.addEventListener("resize", syncPageOffset);
28
+ watchPostEffect(syncPageOffset);
29
+ onScopeDispose(() => {
30
+ observer.disconnect();
31
+ window.removeEventListener("resize", syncPageOffset);
32
+ });
33
+ });
19
34
  const selectedConversation = computed(() => state.conversationHistory.find((entry) => String(entry.id) === state.activeConversationId));
20
35
  const turns = computed(() => mapAssistantConversationTurns(state.messages, state.pendingToolEvents, props));
21
36
  const adapter = computed(() => ({
@@ -67,7 +82,7 @@ defineExpose({ focus: () => conversation.value?.focus() });
67
82
  </script>
68
83
 
69
84
  <template>
70
- <section class="assistant-surface" :class="`assistant-surface--${layout}`" :aria-label="assistantLabel">
85
+ <section ref="root" class="assistant-surface" :class="`assistant-surface--${layout}`" :aria-label="assistantLabel">
71
86
  <div class="assistant-surface__toolbar">
72
87
  <span class="assistant-surface__title">{{ selectedConversation?.title || 'New conversation' }}</span>
73
88
  <v-btn variant="text" @click="historyOpen = true">Conversations</v-btn>
@@ -122,7 +137,7 @@ defineExpose({ focus: () => conversation.value?.focus() });
122
137
 
123
138
  <style scoped>
124
139
  .assistant-surface { display: flex; flex-direction: column; height: 100%; max-height: 100%; min-height: 0; min-width: 0; overflow: hidden; gap: .5rem; }
125
- .assistant-surface--page { padding: .5rem; }
140
+ .assistant-surface--page { padding: .5rem; height: calc(100dvh - var(--assistant-page-top, 0px) - var(--v-layout-bottom, 0px) - 1rem - env(safe-area-inset-bottom, 0px)); }
126
141
  .assistant-surface__toolbar { display: flex; align-items: center; flex-wrap: wrap; flex: 0 0 auto; gap: .25rem; }
127
142
  .assistant-surface__title { flex: 1 1 8rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .875rem; }
128
143
  .assistant-surface__conversation { flex: 1 1 auto; }
@@ -168,14 +168,11 @@ function useAssistantRuntime({ api = null, surfaceId = "" } = {}) {
168
168
  return {
169
169
  targetSurfaceId: normalizeText(assistantSurface.value?.targetSurfaceId).toLowerCase(),
170
170
  userId: normalizeRecordId(placementSnapshot.value?.user?.id, { fallback: null }),
171
- workspaceSlug,
172
- workspaceId: assistantSurface.value?.runtimeSurfaceRequiresWorkspace
173
- ? normalizeRecordId(placementSnapshot.value?.workspace?.id, { fallback: null })
174
- : null
171
+ workspaceSlug
175
172
  };
176
173
  });
177
174
  const hasRuntimeScope = computed(() =>
178
- Boolean(assistantSurface.value) &&
175
+ Boolean(assistantSurface.value && runtimeScope.value.userId) &&
179
176
  (assistantSurface.value?.runtimeSurfaceRequiresWorkspace ? Boolean(runtimeScope.value.workspaceSlug) : true)
180
177
  );
181
178
 
@@ -224,3 +224,37 @@ test("history restores preserve new typing and reject obsolete pages and failed
224
224
  await stopProcess(vite);
225
225
  }
226
226
  });
227
+
228
+ test("page assistant bounds long history in a document layout and preserves drafts when workspace metadata arrives", {
229
+ skip: process.env.JSKIT_ASSISTANT_RUNTIME_BROWSER_INTEGRATION !== "1",
230
+ timeout: 60_000
231
+ }, async () => {
232
+ const vite = await startViteFixture({ fixtureRoot });
233
+ const browser = await chromium.launch(createChromiumLaunchOptions());
234
+ const page = await browser.newPage();
235
+ try {
236
+ await page.goto(`${vite.baseURL}/?page=1`);
237
+ await page.getByRole("button", { name: "Conversations", exact: true }).click();
238
+ await page.getByRole("dialog", { name: "Conversations", exact: true }).getByText("Saved conversation", { exact: true }).click();
239
+ await expect(page.getByText("Saved 1 message 70.", { exact: false })).toBeVisible();
240
+ const input = page.getByRole("textbox", { name: "Message AI assistant" });
241
+ for (const viewport of [{ width: 390, height: 844 }, { width: 800, height: 800 }, { width: 1280, height: 600 }]) {
242
+ await page.setViewportSize(viewport);
243
+ await expect.poll(async () => {
244
+ const bounds = await input.boundingBox();
245
+ return bounds.y + bounds.height;
246
+ }).toBeLessThanOrEqual(viewport.height);
247
+ assert.ok(await page.getByLabel("Conversation messages", { exact: true }).evaluate(element => element.scrollHeight > element.clientHeight));
248
+ }
249
+ await input.fill("Keep this draft while workspace details load.");
250
+ await input.evaluate(element => { element.focus(); element.setSelectionRange(5, 9); });
251
+ await page.getByRole("button", { name: "Load workspace metadata" }).evaluate(element => element.click());
252
+ await expect(input).toHaveValue("Keep this draft while workspace details load.");
253
+ await expect(input).toBeFocused();
254
+ assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [5, 9]);
255
+ await expect(page.getByText("Saved 1 message 70.", { exact: false })).toBeVisible();
256
+ } finally {
257
+ await browser.close();
258
+ await stopProcess(vite);
259
+ }
260
+ });