@jskit-ai/assistant-core 0.1.165 → 0.1.166

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 (63) hide show
  1. package/examples/conversation/App.vue +104 -0
  2. package/examples/conversation/README.md +42 -0
  3. package/examples/conversation/dist/assets/index-CSTXm6q3.css +1 -0
  4. package/examples/conversation/dist/assets/index-DGgOLzoo.js +30 -0
  5. package/examples/conversation/dist/index.html +4 -0
  6. package/examples/conversation/index.html +2 -0
  7. package/examples/conversation/main.js +9 -0
  8. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js +7450 -0
  9. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js.map +1 -0
  10. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js +2311 -0
  11. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js.map +1 -0
  12. package/examples/conversation/node_modules/.vite/deps/_metadata.json +70 -0
  13. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js +710 -0
  14. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js.map +1 -0
  15. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js +1214 -0
  16. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js.map +1 -0
  17. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js +98 -0
  18. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js.map +1 -0
  19. package/examples/conversation/node_modules/.vite/deps/package.json +3 -0
  20. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js +1301 -0
  21. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js.map +1 -0
  22. package/examples/conversation/node_modules/.vite/deps/vue.js +2 -0
  23. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js +8538 -0
  24. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js.map +1 -0
  25. package/examples/conversation/node_modules/.vite/deps/vuetify.js +413 -0
  26. package/examples/conversation/node_modules/.vite/deps/vuetify.js.map +1 -0
  27. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js +25370 -0
  28. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js.map +1 -0
  29. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js +132 -0
  30. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js.map +1 -0
  31. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js +70 -0
  32. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js.map +1 -0
  33. package/examples/conversation/package.json +22 -0
  34. package/examples/conversation/server.js +123 -0
  35. package/examples/conversation/vite.config.js +8 -0
  36. package/package.json +59 -7
  37. package/src/client/conversation/AssistantComposerActions.vue +43 -0
  38. package/src/client/conversation/AssistantConversationElement.vue +78 -0
  39. package/src/client/conversation/AssistantProgress.vue +88 -0
  40. package/src/client/conversation/AssistantPromptInput.vue +467 -0
  41. package/src/client/conversation/AssistantTranscript.vue +1361 -0
  42. package/src/client/conversation/LongTextInlineParts.vue +64 -0
  43. package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
  44. package/src/client/conversation/index.js +9 -0
  45. package/src/client/conversation/submitText.js +29 -0
  46. package/src/client/conversation/useScrollToBottom.js +151 -0
  47. package/src/server/conversation/codexClient.js +328 -0
  48. package/src/server/conversation/codexEvents.js +623 -0
  49. package/src/server/conversation/codexTurn.js +280 -0
  50. package/src/server/conversation/index.js +2 -0
  51. package/src/server/conversation/memoryStorage.js +76 -0
  52. package/src/server/conversation/normalize.js +7 -0
  53. package/src/server/conversation/openCodeClient.js +512 -0
  54. package/src/server/conversation/transcript.js +92 -0
  55. package/src/shared/conversation/answerChoiceSugar.js +191 -0
  56. package/src/shared/conversation/index.js +6 -0
  57. package/src/shared/conversation/longTextBlocks.js +403 -0
  58. package/src/shared/conversation/numberedQuestionSugar.js +295 -0
  59. package/src/shared/conversation/scrollFollowState.js +32 -0
  60. package/src/shared/conversation/thinkingText.js +13 -0
  61. package/src/shared/conversation/turns.js +30 -0
  62. package/src/testing/conversationStorageContract.js +35 -0
  63. package/test/conversation.test.js +57 -0
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div class="assistant-progress">
3
+ <button
4
+ v-if="messages.length > previewCount"
5
+ :aria-expanded="expanded"
6
+ class="assistant-progress__toggle"
7
+ type="button"
8
+ @click="expanded = !expanded"
9
+ >
10
+ {{ toggleLabel }}
11
+ </button>
12
+ <div
13
+ v-for="(message, index) in visibleMessages"
14
+ :key="message.key || message.id || index"
15
+ class="assistant-progress__message"
16
+ >
17
+ {{ message.text }}
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { computed, ref } from "vue";
24
+
25
+ const props = defineProps({
26
+ messages: { type: Array, default: () => [] },
27
+ pending: { type: Boolean, default: false },
28
+ previewLimit: { type: Number, default: 2 }
29
+ });
30
+ const expanded = ref(false);
31
+ const previewCount = computed(() => props.pending ? props.previewLimit : 0);
32
+ const visibleMessages = computed(() => {
33
+ if (expanded.value) {
34
+ return props.messages;
35
+ }
36
+ return previewCount.value > 0 ? props.messages.slice(-previewCount.value) : [];
37
+ });
38
+ const toggleLabel = computed(() => {
39
+ if (expanded.value) {
40
+ return previewCount.value > 0
41
+ ? `Show latest ${previewCount.value} progress updates`
42
+ : "Hide progress updates";
43
+ }
44
+ return `Show all ${props.messages.length} progress ${props.messages.length === 1 ? "update" : "updates"}`;
45
+ });
46
+ </script>
47
+
48
+ <style scoped>
49
+ .assistant-progress {
50
+ color: rgba(var(--v-theme-on-surface), 0.58);
51
+ display: grid;
52
+ font-size: 0.78rem;
53
+ gap: 0.18rem;
54
+ line-height: 1.42;
55
+ min-width: 0;
56
+ overflow-wrap: anywhere;
57
+ }
58
+
59
+ .assistant-progress__message {
60
+ white-space: pre-wrap;
61
+ }
62
+
63
+ .assistant-progress__toggle {
64
+ background: transparent;
65
+ border: 0;
66
+ color: rgb(var(--v-theme-primary));
67
+ cursor: pointer;
68
+ font: inherit;
69
+ justify-self: start;
70
+ padding: 0.12rem 0;
71
+ text-align: left;
72
+ }
73
+
74
+ .assistant-progress__toggle:hover {
75
+ text-decoration: underline;
76
+ }
77
+
78
+ .assistant-progress__toggle:focus-visible {
79
+ outline: 2px solid rgb(var(--v-theme-primary));
80
+ outline-offset: 2px;
81
+ }
82
+
83
+ @media (pointer: coarse) {
84
+ .assistant-progress__toggle {
85
+ min-height: 3rem;
86
+ }
87
+ }
88
+ </style>
@@ -0,0 +1,467 @@
1
+ <template>
2
+ <div
3
+ class="assistant-prompt-input"
4
+ :class="{
5
+ 'assistant-prompt-input--compact': density === 'compact',
6
+ 'assistant-prompt-input--dragging': dragging,
7
+ 'assistant-prompt-input--has-attachments': attachmentState.count,
8
+ 'assistant-prompt-input--has-footer': $slots.footer,
9
+ 'assistant-prompt-input--has-input-start': $slots['input-start']
10
+ }"
11
+ >
12
+ <slot name="attachments" />
13
+
14
+ <div
15
+ class="assistant-prompt-input__field"
16
+ :class="{ 'assistant-prompt-input__field--disabled': disabled }"
17
+ >
18
+ <label
19
+ v-if="label"
20
+ class="assistant-prompt-input__label"
21
+ :for="textareaId"
22
+ >
23
+ {{ label }}
24
+ </label>
25
+
26
+ <div
27
+ v-if="$slots['input-start']"
28
+ class="assistant-prompt-input__input-start"
29
+ >
30
+ <slot name="input-start" />
31
+ </div>
32
+
33
+ <textarea
34
+ :id="textareaId"
35
+ ref="textareaRef"
36
+ :aria-describedby="describedBy || undefined"
37
+ :aria-label="textareaAriaLabel"
38
+ class="assistant-prompt-input__input"
39
+ :disabled="disabled"
40
+ :placeholder="placeholder"
41
+ :rows="rows"
42
+ :value="modelValue"
43
+ @blur="handleTextareaBlur"
44
+ @focus="handleTextareaFocus"
45
+ @input="handleTextareaInput"
46
+ @keydown="handleTextareaKeydown"
47
+ @paste="emit('paste', $event)"
48
+ />
49
+
50
+ <div
51
+ v-if="$slots.footer"
52
+ class="assistant-prompt-input__footer"
53
+ >
54
+ <slot name="footer" :attachment-state="attachmentState" />
55
+ </div>
56
+ </div>
57
+
58
+ <div
59
+ v-if="detailsVisible"
60
+ class="assistant-prompt-input__details"
61
+ >
62
+ <div
63
+ v-for="message in combinedErrorMessages"
64
+ :key="message"
65
+ class="assistant-prompt-input__error"
66
+ >
67
+ {{ message }}
68
+ </div>
69
+ <div
70
+ v-if="hintVisible"
71
+ class="assistant-prompt-input__hint"
72
+ >
73
+ {{ hint }}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <script setup>
80
+ import { computed, onBeforeUnmount, onMounted, ref, useId, watch } from "vue";
81
+ const emit = defineEmits(["blur", "escape", "focus", "input-activity", "submit", "tab-to-submit", "update:modelValue", "paste"]);
82
+ const props = defineProps({
83
+ submitOnModifierEnter: { type: Boolean, default: false },
84
+ ariaLabel: {
85
+ default: "",
86
+ type: String
87
+ },
88
+ attachmentState: { default: () => ({ canSubmit: true, count: 0 }), type: Object },
89
+ dragging: { default: false, type: Boolean },
90
+ autoGrow: {
91
+ default: true,
92
+ type: Boolean
93
+ },
94
+ disabled: {
95
+ default: false,
96
+ type: Boolean
97
+ },
98
+ describedBy: {
99
+ default: "",
100
+ type: String
101
+ },
102
+ errorMessages: {
103
+ default: () => [],
104
+ type: [Array, String]
105
+ },
106
+ hint: {
107
+ default: "",
108
+ type: String
109
+ },
110
+ label: {
111
+ default: "",
112
+ type: String
113
+ },
114
+ modelValue: {
115
+ default: "",
116
+ type: String
117
+ },
118
+ persistentHint: {
119
+ default: false,
120
+ type: Boolean
121
+ },
122
+ placeholderAffectsHeight: {
123
+ default: true,
124
+ type: Boolean
125
+ },
126
+ placeholder: {
127
+ default: "",
128
+ type: String
129
+ },
130
+ rows: {
131
+ default: 4,
132
+ type: [Number, String]
133
+ },
134
+ density: {
135
+ default: "default",
136
+ type: String
137
+ },
138
+ submitOnEnter: {
139
+ default: false,
140
+ type: Boolean
141
+ },
142
+ submitEnabled: {
143
+ default: true,
144
+ type: Boolean
145
+ },
146
+ tabToSubmit: {
147
+ default: false,
148
+ type: Boolean
149
+ },
150
+ variant: {
151
+ default: "outlined",
152
+ type: String
153
+ }
154
+ });
155
+
156
+ const textareaRef = ref(null);
157
+ const textareaId = `assistant-prompt-${useId()}`;
158
+ let resizeFrame = 0;
159
+ let preserveHeightForNextModelValueChange = false;
160
+ const combinedErrorMessages = computed(() => {
161
+ return Array.isArray(props.errorMessages)
162
+ ? props.errorMessages
163
+ : [props.errorMessages].filter(Boolean);
164
+ });
165
+ const hintVisible = computed(() => Boolean(
166
+ props.hint &&
167
+ (props.persistentHint || combinedErrorMessages.value.length < 1)
168
+ ));
169
+ const detailsVisible = computed(() => Boolean(
170
+ combinedErrorMessages.value.length ||
171
+ hintVisible.value
172
+ ));
173
+ const textareaAriaLabel = computed(() => (
174
+ props.ariaLabel
175
+ ? props.ariaLabel
176
+ : undefined
177
+ ));
178
+
179
+ function resizeTextarea() {
180
+ if (!props.autoGrow) {
181
+ return;
182
+ }
183
+ const textarea = textareaRef.value;
184
+ if (!textarea) {
185
+ return;
186
+ }
187
+ if (!props.placeholderAffectsHeight && !textarea.value) {
188
+ return;
189
+ }
190
+ const style = window.getComputedStyle(textarea);
191
+ const minHeight = Number.parseFloat(style.minHeight) || 0;
192
+ const maxHeight = Number.parseFloat(style.maxHeight) || Number.POSITIVE_INFINITY;
193
+ textarea.style.height = "auto";
194
+ const contentHeight = Math.max(textarea.scrollHeight, minHeight);
195
+ const targetHeight = Math.min(contentHeight, maxHeight);
196
+ textarea.style.height = `${targetHeight}px`;
197
+ textarea.style.overflowY = contentHeight > targetHeight + 1 ? "auto" : "hidden";
198
+ }
199
+
200
+ function queueResizeTextarea() {
201
+ if (!props.autoGrow || typeof window === "undefined") {
202
+ return;
203
+ }
204
+ if (resizeFrame) {
205
+ window.cancelAnimationFrame(resizeFrame);
206
+ }
207
+ resizeFrame = window.requestAnimationFrame(() => {
208
+ resizeFrame = 0;
209
+ resizeTextarea();
210
+ });
211
+ }
212
+
213
+ function preserveHeightForNextModelValue() {
214
+ const textarea = textareaRef.value;
215
+ if (!props.autoGrow || !textarea) {
216
+ return false;
217
+ }
218
+ if (resizeFrame) {
219
+ window.cancelAnimationFrame(resizeFrame);
220
+ resizeFrame = 0;
221
+ }
222
+ preserveHeightForNextModelValueChange = true;
223
+ textarea.style.height = `${textarea.getBoundingClientRect().height}px`;
224
+ textarea.style.overflowY = "auto";
225
+ return true;
226
+ }
227
+
228
+ function handleTextareaInput(event = {}) {
229
+ preserveHeightForNextModelValueChange = false;
230
+ emit("input-activity");
231
+ emit("update:modelValue", String(event?.target?.value || ""));
232
+ queueResizeTextarea();
233
+ }
234
+
235
+ function handleTextareaFocus(event = {}) {
236
+ emit("focus", event);
237
+ }
238
+
239
+ function handleTextareaBlur(event = {}) {
240
+ emit("blur", event);
241
+ }
242
+
243
+ function handleTextareaKeydown(event = {}) {
244
+ if (event.key === "Escape") {
245
+ emit("escape", event);
246
+ return;
247
+ }
248
+ if (
249
+ props.tabToSubmit &&
250
+ props.submitEnabled &&
251
+ String(props.modelValue || "").trim() &&
252
+ props.attachmentState.canSubmit &&
253
+ event.key === "Tab" &&
254
+ !event.shiftKey &&
255
+ !event.altKey &&
256
+ !event.ctrlKey &&
257
+ !event.metaKey
258
+ ) {
259
+ event.preventDefault();
260
+ emit("tab-to-submit");
261
+ return;
262
+ }
263
+ if (props.submitOnModifierEnter && event.key === "Enter" && (event.ctrlKey || event.metaKey) &&
264
+ !event.shiftKey && !event.altKey && !event.isComposing && props.submitEnabled && props.attachmentState.canSubmit) {
265
+ event.preventDefault();
266
+ event.stopPropagation();
267
+ emit("submit");
268
+ return;
269
+ }
270
+ if (event.key === "Enter" && !props.submitOnEnter) {
271
+ event.stopPropagation();
272
+ return;
273
+ }
274
+ if (
275
+ !props.submitOnEnter ||
276
+ event.key !== "Enter" ||
277
+ event.shiftKey ||
278
+ event.altKey ||
279
+ event.ctrlKey ||
280
+ event.metaKey ||
281
+ event.isComposing ||
282
+ !props.submitEnabled ||
283
+ !props.attachmentState.canSubmit
284
+ ) {
285
+ return;
286
+ }
287
+ event.preventDefault();
288
+ emit("submit");
289
+ }
290
+
291
+ function focusTextarea(options = { preventScroll: true }) {
292
+ textareaRef.value?.focus?.(options);
293
+ }
294
+
295
+ onMounted(queueResizeTextarea);
296
+
297
+ onBeforeUnmount(() => {
298
+ if (resizeFrame && typeof window !== "undefined") {
299
+ window.cancelAnimationFrame(resizeFrame);
300
+ resizeFrame = 0;
301
+ }
302
+ });
303
+
304
+ watch(() => [
305
+ props.autoGrow,
306
+ props.modelValue,
307
+ props.placeholder,
308
+ props.placeholderAffectsHeight,
309
+ props.rows
310
+ ], (values, previousValues = []) => {
311
+ const modelValueChanged = values[1] !== previousValues[1];
312
+ if (preserveHeightForNextModelValueChange && modelValueChanged) {
313
+ preserveHeightForNextModelValueChange = false;
314
+ return;
315
+ }
316
+ queueResizeTextarea();
317
+ });
318
+
319
+ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightForNextModelValue, queueResizeTextarea });
320
+ </script>
321
+ <style scoped>
322
+ .assistant-prompt-input {
323
+ box-sizing: border-box;
324
+ display: grid;
325
+ gap: 0;
326
+ max-width: 100%;
327
+ min-width: 0;
328
+ overflow: hidden;
329
+ padding: 0;
330
+ position: relative;
331
+ text-align: left;
332
+ width: 100%;
333
+ }
334
+
335
+ .assistant-prompt-input__field {
336
+ background: rgb(var(--v-theme-surface));
337
+ border: 1px solid rgba(var(--v-theme-on-surface), 0.34);
338
+ border-radius: 18px;
339
+ box-sizing: border-box;
340
+ box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.08);
341
+ display: grid;
342
+ max-width: 100%;
343
+ min-width: 0;
344
+ padding-top: 0.01rem;
345
+ transition: border-color 120ms ease, box-shadow 120ms ease;
346
+ }
347
+
348
+ .assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):focus-within {
349
+ border-color: rgb(var(--v-theme-primary));
350
+ box-shadow:
351
+ 0 0 0 2px rgba(var(--v-theme-primary), 0.28),
352
+ inset 0 0 0 1px rgba(var(--v-theme-primary), 0.2);
353
+ }
354
+
355
+ .assistant-prompt-input__field--disabled {
356
+ background: rgba(var(--v-theme-on-surface), 0.04);
357
+ border-color: rgba(var(--v-theme-on-surface), 0.16);
358
+ box-shadow: none;
359
+ }
360
+
361
+ .assistant-prompt-input__label {
362
+ align-self: start;
363
+ background: rgb(var(--v-theme-surface));
364
+ color: rgba(var(--v-theme-on-surface), 0.82);
365
+ font-size: 0.78rem;
366
+ line-height: 1.1;
367
+ margin: -0.5rem 0 0 0.9rem;
368
+ max-width: calc(100% - 1.8rem);
369
+ overflow: hidden;
370
+ padding-inline: 0.24rem;
371
+ text-overflow: ellipsis;
372
+ white-space: nowrap;
373
+ width: fit-content;
374
+ z-index: 1;
375
+ }
376
+
377
+ .assistant-prompt-input__input {
378
+ background: transparent;
379
+ border: 0;
380
+ box-sizing: border-box;
381
+ color: rgb(var(--v-theme-on-surface));
382
+ display: block;
383
+ font: inherit;
384
+ line-height: 1.4;
385
+ max-height: min(16rem, 32dvh);
386
+ min-height: 3.55rem;
387
+ min-width: 0;
388
+ outline: 0;
389
+ overflow-x: hidden;
390
+ overflow-y: hidden;
391
+ padding: 0.5rem 1rem 0.2rem;
392
+ resize: none;
393
+ width: 100%;
394
+ word-break: break-word;
395
+ }
396
+
397
+ .assistant-prompt-input__input-start {
398
+ min-width: 0;
399
+ padding: 0.5rem 1rem 0.08rem;
400
+ }
401
+
402
+ .assistant-prompt-input--has-input-start .assistant-prompt-input__input {
403
+ padding-top: 0.24rem;
404
+ }
405
+
406
+ .assistant-prompt-input__input::placeholder {
407
+ color: rgba(var(--v-theme-on-surface), 0.58);
408
+ opacity: 1;
409
+ }
410
+
411
+ .assistant-prompt-input__input:disabled {
412
+ color: rgba(var(--v-theme-on-surface), 0.38);
413
+ cursor: not-allowed;
414
+ opacity: 1;
415
+ -webkit-text-fill-color: currentColor;
416
+ }
417
+
418
+ .assistant-prompt-input__input:disabled::placeholder {
419
+ color: inherit;
420
+ opacity: 1;
421
+ }
422
+
423
+ .assistant-prompt-input__footer {
424
+ min-width: 0;
425
+ padding: 0 0.55rem 0.55rem;
426
+ }
427
+
428
+ .assistant-prompt-input--compact .assistant-prompt-input__input {
429
+ max-height: min(8rem, 20dvh);
430
+ min-height: 2.5rem;
431
+ padding: 0.4rem 0.65rem 0.15rem;
432
+ }
433
+
434
+ .assistant-prompt-input--compact .assistant-prompt-input__footer {
435
+ padding: 0 0.3rem 0.25rem;
436
+ }
437
+
438
+ .assistant-prompt-input__details {
439
+ color: rgba(var(--v-theme-on-surface), 0.62);
440
+ display: grid;
441
+ font-size: 0.76rem;
442
+ gap: 0.12rem;
443
+ line-height: 1.3;
444
+ min-width: 0;
445
+ padding: 0.32rem 0.75rem 0;
446
+ }
447
+
448
+ .assistant-prompt-input__error {
449
+ color: rgb(var(--v-theme-error));
450
+ }
451
+
452
+ .assistant-prompt-input--dragging {
453
+ outline: 2px dashed rgb(var(--v-theme-primary));
454
+ outline-offset: 4px;
455
+ }
456
+
457
+ .assistant-prompt-input__file-input {
458
+ display: none;
459
+ }
460
+
461
+ .assistant-prompt-input--has-attachments .assistant-prompt-input__field {
462
+ border-top-left-radius: 0;
463
+ border-top-right-radius: 0;
464
+ margin-top: -1px;
465
+ }
466
+
467
+ </style>