@jskit-ai/assistant-core 0.1.205 → 0.1.207
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/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +12 -2
- package/package.json +19 -9
- package/src/client/conversation/AssistantTranscript.vue +177 -16
- package/src/server/conversation/claudeTurn.js +16 -7
- package/src/server/conversation/codexConversations.js +94 -0
- package/src/server/conversation/codexHistoryAdapter.js +37 -0
- package/src/server/conversation/codexProvider.js +15 -5
- package/src/server/conversation/codexProviderOwner.js +70 -47
- package/src/server/conversation/codexProviderSelection.js +5 -2
- package/src/server/conversation/codexRuntimeLifecycle.js +2 -1
- package/src/server/conversation/codexTurn.js +10 -1
- package/src/server/conversation/codexTurnControl.js +1 -1
- package/src/server/conversation/codexTurnRecovery.js +21 -8
- package/src/server/conversation/openCodeRuntime.js +107 -64
- package/src/server/conversation/openCodeTurn.js +33 -16
- package/src/server/conversation/providers/api.js +1 -0
- package/src/server/conversation/providers/claudeDriver.js +13 -0
- package/src/server/conversation/providers/codex.js +90 -47
- package/src/server/conversation/providers/codexDriver.js +4 -1
- package/src/server/conversation/providers/opencodeDriver.js +3 -0
- package/src/server/conversation/runtime.js +32 -5
- package/src/server/hosts/codexEvents.js +15 -0
- package/src/server/hosts/codexTurn.js +5 -0
- package/src/server/hosts/openCodeClient.js +4 -0
- package/src/server/lib/aiConnectionClient.js +9 -11
- package/test/aiConnectionClient.test.js +209 -0
- package/test/assistantScroll.browser.test.js +255 -1
- package/test/claudeConversationSession.test.js +78 -0
- package/test/claudeTurn.test.js +18 -0
- package/test/codexConversationSession.test.js +318 -0
- package/test/codexHistoryAdapter.test.js +106 -0
- package/test/codexTurn.test.js +203 -2
- package/test/conversationConfiguration.test.js +27 -0
- package/test/conversationReplacement.test.js +76 -1
- package/test/conversationRuntime.test.js +173 -4
- package/test/conversationSession.test.js +139 -0
- package/test/conversationTools.test.js +17 -3
- package/test/opencodeConversationSession.test.js +200 -1
- package/test/packageMetadata.test.js +75 -0
- package/test/serviceToolCatalog.test.js +31 -0
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.207",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.227",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.229",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.46",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.46"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -4,8 +4,12 @@ import AssistantConversationElement from "../../src/client/conversation/Assistan
|
|
|
4
4
|
import { conversationTurnsFromMessages } from "../../src/shared/conversation/turns.js";
|
|
5
5
|
import { mergeConversationStream } from "../../src/shared/conversation/streaming.js";
|
|
6
6
|
const draft = ref("");
|
|
7
|
-
const
|
|
7
|
+
const error = ref("");
|
|
8
|
+
const reloads = ref(0);
|
|
8
9
|
const query = new URLSearchParams(location.search);
|
|
10
|
+
const visible = ref(!query.has("initial-hidden"));
|
|
11
|
+
const retainWhenHidden = query.has("retain-hidden");
|
|
12
|
+
const mounted = ref(true);
|
|
9
13
|
const controls = query.has("controls");
|
|
10
14
|
const deferredHistory = query.has("history");
|
|
11
15
|
const pagedHistory = query.has("paged-history");
|
|
@@ -52,12 +56,16 @@ function finishHistory(error = "") {
|
|
|
52
56
|
const adapter = computed(() => ({
|
|
53
57
|
conversation: {
|
|
54
58
|
visible: visible.value,
|
|
59
|
+
retainWhenHidden,
|
|
60
|
+
error: error.value,
|
|
61
|
+
errorReloadable: true,
|
|
55
62
|
turns: mergeConversationStream(conversationTurnsFromMessages(messages.value), { messages: [liveMessage.value] }), scrollKey: scope.value,
|
|
56
63
|
hasMoreBefore: hasMoreBefore.value, loadingMore: loadingMore.value, loadMoreError: loadMoreError.value
|
|
57
64
|
},
|
|
58
65
|
composer: { draft: draft.value, canSend: Boolean(draft.value.trim()), rows: 2 },
|
|
59
66
|
actions: {
|
|
60
67
|
setDraft(value) { draft.value = value; },
|
|
68
|
+
reload() { reloads.value += 1; error.value = ""; },
|
|
61
69
|
submit() { draft.value = ""; },
|
|
62
70
|
loadMore({ complete }) {
|
|
63
71
|
historyRequests.value += 1;
|
|
@@ -81,6 +89,8 @@ const adapter = computed(() => ({
|
|
|
81
89
|
<button @click="visible = !visible">Toggle visibility</button>
|
|
82
90
|
<button @click="scope = scope === 'one' ? 'two' : 'one'">Change conversation</button>
|
|
83
91
|
<button @click="messages.push({ id: `added-${messages.length}`, role: 'assistant', text: 'New reply. '.repeat(30), status: 'completed' })">Append reply</button>
|
|
92
|
+
<button v-if="query.has('update-errors')" @click="error = 'Conversation updates are temporarily unavailable. Reload to reconnect; the assistant may still be working.'">Show update error</button>
|
|
93
|
+
<button v-if="retainWhenHidden" @click="mounted = !mounted">Toggle mount</button>
|
|
84
94
|
</nav>
|
|
85
95
|
<nav v-if="deferredHistory">
|
|
86
96
|
<button :disabled="!loadingMore" @click="finishHistory()">Complete history load</button>
|
|
@@ -88,7 +98,7 @@ const adapter = computed(() => ({
|
|
|
88
98
|
<button @click="hasMoreBefore = false">Exhaust history</button>
|
|
89
99
|
<output>History requests: {{ historyRequests }}</output>
|
|
90
100
|
</nav>
|
|
91
|
-
<AssistantConversationElement v-show="visible" :adapter="adapter" class="assistant-responsive-fixture__conversation" />
|
|
101
|
+
<AssistantConversationElement v-if="mounted" v-show="visible" :adapter="adapter" :data-error-reloads="reloads" class="assistant-responsive-fixture__conversation" />
|
|
92
102
|
</main>
|
|
93
103
|
</v-main>
|
|
94
104
|
</v-app>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.207",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"./server/codex-provider": "./src/server/hosts/codexProvider.js",
|
|
33
33
|
"./server/native-history": "./src/server/hosts/nativeHistory.js",
|
|
34
34
|
"./testing/native-codex": "./src/testing/nativeCodex.js",
|
|
35
|
-
"./testing/native-opencode": "./src/testing/nativeOpenCode.js"
|
|
35
|
+
"./testing/native-opencode": "./src/testing/nativeOpenCode.js",
|
|
36
|
+
"./server/codex-client": "./src/server/conversation/codexClient.js",
|
|
37
|
+
"./client/conversation-submit": "./src/client/conversation/submitText.js"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"@ai-sdk/anthropic": "^4.0.53",
|
|
@@ -45,8 +47,8 @@
|
|
|
45
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
46
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
47
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
48
|
-
"@jskit-ai/resource-core": "0.1.
|
|
49
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.171",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.171",
|
|
50
52
|
"@mdi/js": "^7.4.47",
|
|
51
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
52
54
|
"ai": "^7.0.101",
|
|
@@ -55,8 +57,8 @@
|
|
|
55
57
|
"ws": "^8.20.1"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
58
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
59
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.227",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.229",
|
|
60
62
|
"vue": "^3.5.13",
|
|
61
63
|
"vuetify": "^4.0.0"
|
|
62
64
|
},
|
|
@@ -126,15 +128,15 @@
|
|
|
126
128
|
},
|
|
127
129
|
{
|
|
128
130
|
"subpath": "./server/codex-events",
|
|
129
|
-
"summary": "Advanced native host: Codex event
|
|
131
|
+
"summary": "Advanced native host: Original Codex event classification, text, status and identity helpers, thread-history projections and context-refresh facts."
|
|
130
132
|
},
|
|
131
133
|
{
|
|
132
134
|
"subpath": "./server/codex-turn",
|
|
133
|
-
"summary": "Advanced native host: Original Codex run, receipt, observation, goal, steering, control, scoped-turn and renewal coordination."
|
|
135
|
+
"summary": "Advanced native host: Original detached-turn watcher and status predicates, plus Codex run, receipt, observation, goal, steering, control, scoped-turn and renewal coordination."
|
|
134
136
|
},
|
|
135
137
|
{
|
|
136
138
|
"subpath": "./server/opencode-client",
|
|
137
|
-
"summary": "Advanced native host: OpenCode native history retirement
|
|
139
|
+
"summary": "Advanced native host: Original OpenCode loopback client, bounded response and assistant text helpers, plus native history retirement."
|
|
138
140
|
},
|
|
139
141
|
{
|
|
140
142
|
"subpath": "./server/claude-history",
|
|
@@ -187,6 +189,14 @@
|
|
|
187
189
|
{
|
|
188
190
|
"subpath": "./testing/native-opencode",
|
|
189
191
|
"summary": "Testing: Original OpenCode loopback client for isolated host integration tests; tests own endpoints, authorization and cleanup."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"subpath": "./server/codex-client",
|
|
195
|
+
"summary": "Advanced native host: Original Codex JSON-RPC client and endpoint socket-path helper; hosts retain authorization, transport limits and native lifecycle coordination."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"subpath": "./client/conversation-submit",
|
|
199
|
+
"summary": "Optional UI: Original text submission helper forwards companion text through the application draft and canonical send action; also exported by ./client/conversation."
|
|
190
200
|
}
|
|
191
201
|
],
|
|
192
202
|
"capabilities": []
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section
|
|
3
|
-
v-if="visible"
|
|
3
|
+
v-if="visible || retainWhenHidden"
|
|
4
|
+
v-show="visible"
|
|
4
5
|
class="assistant-transcript"
|
|
5
|
-
:class="`assistant-transcript--${variant}
|
|
6
|
+
:class="[`assistant-transcript--${variant}`, { 'assistant-transcript--retained': retainWhenHidden, 'assistant-transcript--error': error }]"
|
|
6
7
|
aria-label="Conversation history"
|
|
7
8
|
>
|
|
8
9
|
<v-btn
|
|
@@ -39,15 +40,23 @@
|
|
|
39
40
|
|
|
40
41
|
<v-alert
|
|
41
42
|
v-if="error"
|
|
43
|
+
class="assistant-transcript__error"
|
|
42
44
|
density="compact"
|
|
43
45
|
type="warning"
|
|
44
46
|
variant="tonal"
|
|
45
47
|
>
|
|
46
|
-
|
|
48
|
+
<div class="assistant-transcript__error-content">
|
|
49
|
+
<span>{{ error }}</span>
|
|
50
|
+
<v-btn
|
|
51
|
+
v-if="errorReloadable && !reloadable" :disabled="reloading" :min-height="48" size="small"
|
|
52
|
+
type="button" variant="text" @click="emit('reload')"
|
|
53
|
+
>
|
|
54
|
+
Reload chat
|
|
55
|
+
</v-btn>
|
|
56
|
+
</div>
|
|
47
57
|
</v-alert>
|
|
48
58
|
|
|
49
59
|
<div
|
|
50
|
-
v-else
|
|
51
60
|
ref="bodyElement"
|
|
52
61
|
aria-label="Conversation messages"
|
|
53
62
|
class="assistant-transcript__body"
|
|
@@ -337,6 +346,10 @@ const props = defineProps({
|
|
|
337
346
|
default: "",
|
|
338
347
|
type: String
|
|
339
348
|
},
|
|
349
|
+
errorReloadable: {
|
|
350
|
+
default: false,
|
|
351
|
+
type: Boolean
|
|
352
|
+
},
|
|
340
353
|
followLatestKey: {
|
|
341
354
|
default: 0,
|
|
342
355
|
type: [Number, String]
|
|
@@ -365,6 +378,10 @@ const props = defineProps({
|
|
|
365
378
|
default: false,
|
|
366
379
|
type: Boolean
|
|
367
380
|
},
|
|
381
|
+
retainWhenHidden: {
|
|
382
|
+
default: false,
|
|
383
|
+
type: Boolean
|
|
384
|
+
},
|
|
368
385
|
scrollKey: {
|
|
369
386
|
default: "",
|
|
370
387
|
type: [Number, String]
|
|
@@ -406,6 +423,7 @@ let liveScrollFrame = 0;
|
|
|
406
423
|
let userScrollIntentTimer = null;
|
|
407
424
|
let initialScrollVersion = 0;
|
|
408
425
|
let loadMoreScrollSnapshot = null;
|
|
426
|
+
let retainedReaderScrollSnapshot = null;
|
|
409
427
|
let loadMoreRequestVersion = 0;
|
|
410
428
|
let pendingTailFollow = false;
|
|
411
429
|
let previousScrollTop = 0;
|
|
@@ -649,7 +667,6 @@ function messageScrollKey(message = null) {
|
|
|
649
667
|
}
|
|
650
668
|
|
|
651
669
|
const timelineScrollTrigger = computed(() => [
|
|
652
|
-
props.error ? "error" : "body",
|
|
653
670
|
loadingIndicatorVisible.value ? "loading" : "ready",
|
|
654
671
|
displayEntries.value.length ? "has-messages" : "empty",
|
|
655
672
|
props.scrollKey
|
|
@@ -698,6 +715,7 @@ function resumePendingTailFollow() {
|
|
|
698
715
|
}
|
|
699
716
|
|
|
700
717
|
function markUserScrollIntent() {
|
|
718
|
+
retainedReaderScrollSnapshot = null;
|
|
701
719
|
userScrollIntent.value = true;
|
|
702
720
|
clearLiveBottomScroll();
|
|
703
721
|
clearScheduledScrolls();
|
|
@@ -735,6 +753,7 @@ function scrollToLatestMessageAfterLayout({
|
|
|
735
753
|
force = false
|
|
736
754
|
} = {}) {
|
|
737
755
|
if (force) {
|
|
756
|
+
retainedReaderScrollSnapshot = null;
|
|
738
757
|
followingLatest.value = true;
|
|
739
758
|
clearUserScrollIntent();
|
|
740
759
|
}
|
|
@@ -747,6 +766,9 @@ function queueInitialBottomScroll() {
|
|
|
747
766
|
const version = initialScrollVersion + 1;
|
|
748
767
|
initialScrollVersion = version;
|
|
749
768
|
initialScrollSettled.value = false;
|
|
769
|
+
if (props.retainWhenHidden && !props.visible) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
750
772
|
void scrollToLatestMessageAfterLayout({
|
|
751
773
|
behavior: "auto",
|
|
752
774
|
force: true
|
|
@@ -765,10 +787,14 @@ function queueLiveBottomScroll({
|
|
|
765
787
|
return;
|
|
766
788
|
}
|
|
767
789
|
if (force) {
|
|
790
|
+
retainedReaderScrollSnapshot = null;
|
|
768
791
|
pendingTailFollow = false;
|
|
769
792
|
followingLatest.value = true;
|
|
770
793
|
clearUserScrollIntent();
|
|
771
794
|
}
|
|
795
|
+
if (props.retainWhenHidden && !props.visible) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
772
798
|
if (liveScrollFrame) {
|
|
773
799
|
return;
|
|
774
800
|
}
|
|
@@ -794,14 +820,22 @@ function clearLiveBottomScroll() {
|
|
|
794
820
|
}
|
|
795
821
|
|
|
796
822
|
function updateLatestFollowFromScroll(event = {}) {
|
|
823
|
+
if (props.retainWhenHidden && !props.visible) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
797
826
|
const target = event?.currentTarget || bodyElement.value;
|
|
827
|
+
if (retainedReaderScrollSnapshot?.restoring) {
|
|
828
|
+
previousScrollTop = target?.scrollTop || 0;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
798
831
|
const scrollTop = target?.scrollTop || 0;
|
|
799
832
|
const scrolledUp = scrollTop < previousScrollTop;
|
|
800
833
|
previousScrollTop = scrollTop;
|
|
801
834
|
// Keep following the reader while the request is pending, before rows prepend.
|
|
802
|
-
if (loadMoreScrollSnapshot &&
|
|
835
|
+
if (loadMoreScrollSnapshot && !loadMoreScrollSnapshot.completed) {
|
|
803
836
|
loadMoreScrollSnapshot.scrollTop = scrollTop;
|
|
804
837
|
loadMoreScrollSnapshot.anchor = visibleHistoryAnchor(target);
|
|
838
|
+
loadMoreScrollSnapshot.scrollHeight = target.scrollHeight;
|
|
805
839
|
}
|
|
806
840
|
const shouldFollow = scrollElementNearBottom(target);
|
|
807
841
|
if (!shouldFollow && !userScrollIntent.value && followingLatest.value) {
|
|
@@ -836,8 +870,42 @@ function visibleHistoryAnchor(element) {
|
|
|
836
870
|
return null;
|
|
837
871
|
}
|
|
838
872
|
|
|
873
|
+
async function restoreRetainedReaderScroll() {
|
|
874
|
+
const snapshot = retainedReaderScrollSnapshot;
|
|
875
|
+
snapshot.restoring = true;
|
|
876
|
+
await nextTick();
|
|
877
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
878
|
+
const element = bodyElement.value;
|
|
879
|
+
if (
|
|
880
|
+
retainedReaderScrollSnapshot !== snapshot ||
|
|
881
|
+
element !== snapshot.body ||
|
|
882
|
+
snapshot.scrollKey !== props.scrollKey ||
|
|
883
|
+
!props.retainWhenHidden ||
|
|
884
|
+
!props.visible ||
|
|
885
|
+
followingLatest.value ||
|
|
886
|
+
loadMoreScrollSnapshot
|
|
887
|
+
) {
|
|
888
|
+
if (retainedReaderScrollSnapshot === snapshot) retainedReaderScrollSnapshot = null;
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const anchor = snapshot.anchor;
|
|
892
|
+
if (anchor && element.contains(anchor.element)) {
|
|
893
|
+
// Materialize the retained row before reading the browser's adjusted scrollTop.
|
|
894
|
+
const offset = anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top;
|
|
895
|
+
previousScrollTop = element.scrollTop + offset - anchor.offset;
|
|
896
|
+
} else {
|
|
897
|
+
previousScrollTop = snapshot.scrollTop + (element.scrollHeight - snapshot.scrollHeight);
|
|
898
|
+
}
|
|
899
|
+
element.scrollTop = previousScrollTop;
|
|
900
|
+
if (pass === 0 && typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
|
|
901
|
+
await new Promise(resolve => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (retainedReaderScrollSnapshot === snapshot) retainedReaderScrollSnapshot = null;
|
|
905
|
+
}
|
|
906
|
+
|
|
839
907
|
function requestLoadMore() {
|
|
840
|
-
if (!props.hasMoreBefore || props.loadingMore || loadMoreScrollSnapshot) {
|
|
908
|
+
if ((props.retainWhenHidden && !props.visible) || !props.hasMoreBefore || props.loadingMore || loadMoreScrollSnapshot) {
|
|
841
909
|
return;
|
|
842
910
|
}
|
|
843
911
|
followingLatest.value = false;
|
|
@@ -876,24 +944,43 @@ async function completeLoadMoreRequest(version, changed) {
|
|
|
876
944
|
clearLoadMoreScrollSnapshot();
|
|
877
945
|
return;
|
|
878
946
|
}
|
|
947
|
+
snapshot.completed = true;
|
|
879
948
|
await nextTick();
|
|
880
949
|
if (loadMoreScrollSnapshot?.version !== version) {
|
|
881
950
|
return;
|
|
882
951
|
}
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
952
|
+
const passes = props.retainWhenHidden ? 2 : 1;
|
|
953
|
+
for (let pass = 0; pass < passes; pass += 1) {
|
|
954
|
+
if (
|
|
955
|
+
(props.retainWhenHidden && !props.visible) ||
|
|
956
|
+
loadMoreScrollSnapshot?.version !== version ||
|
|
957
|
+
snapshot.scrollKey !== props.scrollKey
|
|
958
|
+
) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const element = bodyElement.value;
|
|
962
|
+
if (element) {
|
|
963
|
+
// Offscreen turns use estimated heights. Preserve the visible turn instead
|
|
964
|
+
// of moving by the total height difference, which can change during layout.
|
|
965
|
+
const anchor = snapshot.anchor;
|
|
966
|
+
if (anchor && element.contains(anchor.element)) {
|
|
967
|
+
const offset = anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top;
|
|
968
|
+
previousScrollTop = element.scrollTop + offset - anchor.offset;
|
|
969
|
+
} else {
|
|
970
|
+
previousScrollTop = snapshot.scrollTop + (element.scrollHeight - snapshot.scrollHeight);
|
|
971
|
+
}
|
|
972
|
+
element.scrollTop = previousScrollTop;
|
|
973
|
+
}
|
|
974
|
+
if (pass + 1 < passes && typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
|
|
975
|
+
// Reopening materializes previously hidden content-visibility estimates.
|
|
976
|
+
await new Promise(resolve => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
|
|
977
|
+
}
|
|
892
978
|
}
|
|
893
979
|
clearLoadMoreScrollSnapshot();
|
|
894
980
|
}
|
|
895
981
|
|
|
896
982
|
onBeforeUnmount(() => {
|
|
983
|
+
retainedReaderScrollSnapshot = null;
|
|
897
984
|
clearLiveBottomScroll();
|
|
898
985
|
clearUserScrollIntent();
|
|
899
986
|
clearLoadMoreScrollSnapshot();
|
|
@@ -936,6 +1023,7 @@ watch(() => [
|
|
|
936
1023
|
});
|
|
937
1024
|
|
|
938
1025
|
watch(timelineScrollTrigger, () => {
|
|
1026
|
+
retainedReaderScrollSnapshot = null;
|
|
939
1027
|
clearLoadMoreScrollSnapshot();
|
|
940
1028
|
queueInitialBottomScroll();
|
|
941
1029
|
}, {
|
|
@@ -943,7 +1031,50 @@ watch(timelineScrollTrigger, () => {
|
|
|
943
1031
|
immediate: true
|
|
944
1032
|
});
|
|
945
1033
|
|
|
1034
|
+
watch(() => props.error, () => {
|
|
1035
|
+
if (followingLatest.value) queueLiveBottomScroll();
|
|
1036
|
+
}, { flush: "post" });
|
|
1037
|
+
|
|
1038
|
+
// Capture before this transcript or its parent's v-show removes its geometry.
|
|
946
1039
|
watch(() => props.visible, (visible) => {
|
|
1040
|
+
if (visible) return;
|
|
1041
|
+
retainedReaderScrollSnapshot = null;
|
|
1042
|
+
const element = bodyElement.value;
|
|
1043
|
+
if (
|
|
1044
|
+
props.retainWhenHidden &&
|
|
1045
|
+
initialScrollSettled.value &&
|
|
1046
|
+
!followingLatest.value &&
|
|
1047
|
+
!loadMoreScrollSnapshot &&
|
|
1048
|
+
element?.clientHeight > 0
|
|
1049
|
+
) {
|
|
1050
|
+
retainedReaderScrollSnapshot = {
|
|
1051
|
+
anchor: visibleHistoryAnchor(element),
|
|
1052
|
+
body: element,
|
|
1053
|
+
scrollHeight: element.scrollHeight,
|
|
1054
|
+
scrollKey: props.scrollKey,
|
|
1055
|
+
scrollTop: element.scrollTop
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
}, { flush: "sync" });
|
|
1059
|
+
|
|
1060
|
+
watch(() => props.visible, (visible) => {
|
|
1061
|
+
if (props.retainWhenHidden) {
|
|
1062
|
+
if (!visible) {
|
|
1063
|
+
clearLiveBottomScroll();
|
|
1064
|
+
clearScheduledScrolls();
|
|
1065
|
+
clearUserScrollIntent();
|
|
1066
|
+
initialScrollVersion += 1;
|
|
1067
|
+
} else if (loadMoreScrollSnapshot?.completed) {
|
|
1068
|
+
void completeLoadMoreRequest(loadMoreScrollSnapshot.version, true);
|
|
1069
|
+
} else if (!initialScrollSettled.value) {
|
|
1070
|
+
queueInitialBottomScroll();
|
|
1071
|
+
} else if (retainedReaderScrollSnapshot) {
|
|
1072
|
+
void restoreRetainedReaderScroll();
|
|
1073
|
+
} else {
|
|
1074
|
+
queueLiveBottomScroll();
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
947
1078
|
clearLoadMoreScrollSnapshot();
|
|
948
1079
|
if (visible) queueLiveBottomScroll({ force: true });
|
|
949
1080
|
}, { flush: "post" });
|
|
@@ -971,6 +1102,31 @@ watch(() => props.visible, (visible) => {
|
|
|
971
1102
|
z-index: 1;
|
|
972
1103
|
}
|
|
973
1104
|
|
|
1105
|
+
.assistant-transcript--error {
|
|
1106
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.assistant-transcript__error {
|
|
1110
|
+
font-size: 0.85rem;
|
|
1111
|
+
min-width: 0;
|
|
1112
|
+
overflow-wrap: anywhere;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.assistant-transcript__error-content {
|
|
1116
|
+
align-items: flex-start;
|
|
1117
|
+
display: flex;
|
|
1118
|
+
gap: 0.5rem;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.assistant-transcript__error-content > span {
|
|
1122
|
+
flex: 1 1 auto;
|
|
1123
|
+
min-width: 0;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.assistant-transcript__error-content > .v-btn {
|
|
1127
|
+
flex: 0 0 auto;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
974
1130
|
.assistant-transcript__settling {
|
|
975
1131
|
background: rgb(var(--v-theme-surface));
|
|
976
1132
|
inset: 0;
|
|
@@ -1056,6 +1212,11 @@ watch(() => props.visible, (visible) => {
|
|
|
1056
1212
|
min-width: 0;
|
|
1057
1213
|
}
|
|
1058
1214
|
|
|
1215
|
+
.assistant-transcript--retained .assistant-transcript__turn {
|
|
1216
|
+
/* Late offscreen height estimates cannot move a restored reader anchor. */
|
|
1217
|
+
content-visibility: visible;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1059
1220
|
.assistant-transcript__message-row {
|
|
1060
1221
|
align-items: start;
|
|
1061
1222
|
display: grid;
|
|
@@ -903,9 +903,17 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
903
903
|
if (frame.state === "queued" || frame.type === "user") {
|
|
904
904
|
const pending = admissions.get(uuid);
|
|
905
905
|
if (pending) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
pending.
|
|
906
|
+
// Once acknowledged, storage owns the admission decision. Its write
|
|
907
|
+
// must not race a native-ack timeout and a second submission.
|
|
908
|
+
clearTimeout(pending.timer);
|
|
909
|
+
try {
|
|
910
|
+
await pending.accept();
|
|
911
|
+
admissions.delete(uuid);
|
|
912
|
+
pending.resolve();
|
|
913
|
+
} catch (error) {
|
|
914
|
+
pending.reject(error);
|
|
915
|
+
throw error;
|
|
916
|
+
}
|
|
909
917
|
}
|
|
910
918
|
await publish({ type: "admitted", messageId: uuid, userEcho: frame.type === "user" });
|
|
911
919
|
}
|
|
@@ -959,10 +967,11 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
959
967
|
await publishMessage(block);
|
|
960
968
|
}
|
|
961
969
|
} else if (frame.type === "result") {
|
|
962
|
-
|
|
963
|
-
checkOutput(
|
|
970
|
+
const output = typeof frame.structured_output === "object" ? JSON.stringify(frame.structured_output) : String(frame.result || "");
|
|
971
|
+
checkOutput(output);
|
|
964
972
|
const failed = frame.is_error || frame.subtype !== "success";
|
|
965
|
-
const message = failed ? (frame.errors || []).join("; ") ||
|
|
973
|
+
const message = failed ? (frame.errors || []).join("; ") || output || frame.subtype : "";
|
|
974
|
+
result = failed ? "" : output;
|
|
966
975
|
if (result && ![...messages.values()].some((block) => block.complete && block.role === "assistant" && block.text === result)) {
|
|
967
976
|
await publishMessage({ id: `claude_${frame.uuid || turnId}_result`, role: "assistant", text: result, complete: true });
|
|
968
977
|
}
|
|
@@ -1038,12 +1047,12 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1038
1047
|
if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
|
|
1039
1048
|
const admitted = Promise.withResolvers();
|
|
1040
1049
|
void admitted.promise.catch(() => {});
|
|
1041
|
-
admissions.set(uuid, { ...admitted, accept });
|
|
1042
1050
|
inFlight.add(uuid);
|
|
1043
1051
|
const timer = setTimeout(() => admitted.reject(Object.assign(
|
|
1044
1052
|
failure("Claude has not acknowledged this prompt. Its delivery is uncertain.", "assistant_claude_admission_unknown"),
|
|
1045
1053
|
{ delivery: "uncertain" }
|
|
1046
1054
|
)), timeoutMs);
|
|
1055
|
+
admissions.set(uuid, { ...admitted, accept, timer });
|
|
1047
1056
|
try {
|
|
1048
1057
|
await Promise.all([client.send(message, { messageId: uuid, sessionId: conversationId }), admitted.promise]);
|
|
1049
1058
|
} finally {
|
|
@@ -298,6 +298,99 @@ export function createCodexConversationCommands({
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
async function runDetachedCodexAppServerConversation(sessionId, input = {}, options = {}) {
|
|
302
|
+
const preparation = conversationPreparation.detached(sessionId, input);
|
|
303
|
+
const { admission } = preparation;
|
|
304
|
+
if (admission.ok === false) return admission;
|
|
305
|
+
try {
|
|
306
|
+
return await codexAppServerConversationResult(async () => {
|
|
307
|
+
const unavailable = preparation.result;
|
|
308
|
+
if (unavailable) return unavailable;
|
|
309
|
+
const prompt = preparation.prompt;
|
|
310
|
+
const context = await codexAppServerConversationContext(sessionId, input, options);
|
|
311
|
+
if (context.ok === false) return context;
|
|
312
|
+
const { provider, runtime, workdir } = context;
|
|
313
|
+
const executionProfile = input.executionProfile && typeof input.executionProfile === "object" &&
|
|
314
|
+
!Array.isArray(input.executionProfile) ? input.executionProfile : null;
|
|
315
|
+
const helperTurn = Boolean(executionProfile);
|
|
316
|
+
const emitDetachedEvent = event => {
|
|
317
|
+
if (typeof options.onEvent === "function") options.onEvent(event);
|
|
318
|
+
};
|
|
319
|
+
const onRetired = ({ threadId }) => emitDetachedEvent({ threadId, type: "thread-retired" });
|
|
320
|
+
if (helperTurn) {
|
|
321
|
+
await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
|
|
322
|
+
}
|
|
323
|
+
const execution = preparation.execution(context);
|
|
324
|
+
const threadSettings = helperTurn ? null : await prepareCodexAppServerConversationThread(
|
|
325
|
+
provider, execution.threadPreparation, conversationPreparation.isolation
|
|
326
|
+
);
|
|
327
|
+
const requestedThreadId = normalizeCodexRunText(input.threadId || input.codexSessionId);
|
|
328
|
+
let helper = null;
|
|
329
|
+
let thread;
|
|
330
|
+
if (helperTurn) {
|
|
331
|
+
const scope = { executionProfile, onRetired, projectRuntimeRoot: normalizeCodexRunText(runtime?.stateRoot),
|
|
332
|
+
projectContextRoot: normalizeCodexRunText(runtime?.projectContextRoot), provider, sessionId, requestedThreadId, workdir,
|
|
333
|
+
...(requestedThreadId ? { record: helperLifecycle.threadForOperation({ executionProfile,
|
|
334
|
+
projectRuntimeRoot: normalizeCodexRunText(runtime?.stateRoot), provider, sessionId,
|
|
335
|
+
threadId: requestedThreadId, workdir }) } : {}) };
|
|
336
|
+
helper = await helperLifecycle.prepareDetached(scope, execution.helperPreparation);
|
|
337
|
+
thread = helper.thread;
|
|
338
|
+
} else {
|
|
339
|
+
thread = await acquireCodexAppServerDetachedThread(provider, requestedThreadId, threadSettings, input);
|
|
340
|
+
}
|
|
341
|
+
const threadId = codexAppServerDetachedThreadId(thread, requestedThreadId);
|
|
342
|
+
emitDetachedEvent({ threadId, type: "thread" });
|
|
343
|
+
const requestedTimeoutMs = Number(input.timeoutMs || 0);
|
|
344
|
+
const profileTimeoutMs = Number(executionProfile?.limits?.timeoutMs || 0);
|
|
345
|
+
let turnId = "";
|
|
346
|
+
const result = await runDetachedCodexAppServerTurn({
|
|
347
|
+
provider, threadId, onEvent: emitDetachedEvent,
|
|
348
|
+
timeoutMs: helperTurn
|
|
349
|
+
? Math.min(requestedTimeoutMs > 0 ? requestedTimeoutMs : profileTimeoutMs, profileTimeoutMs)
|
|
350
|
+
: requestedTimeoutMs > 0 ? requestedTimeoutMs : CODEX_APP_SERVER_DETACHED_TURN_TIMEOUT_MS,
|
|
351
|
+
async onFailure(error, status) {
|
|
352
|
+
await helper?.discard();
|
|
353
|
+
return execution.failure(error, status);
|
|
354
|
+
}
|
|
355
|
+
}, async failDispatch => {
|
|
356
|
+
let dispatched;
|
|
357
|
+
if (helperTurn) {
|
|
358
|
+
dispatched = await helper.dispatch(threadId, failDispatch);
|
|
359
|
+
} else {
|
|
360
|
+
try {
|
|
361
|
+
dispatched = await dispatchCodexAppServerDetachedTurn({ provider, prompt, threadId }, execution.authorized);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
await failDispatch(error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
turnId = dispatched.turnId;
|
|
367
|
+
return dispatched;
|
|
368
|
+
});
|
|
369
|
+
if (helperTurn) {
|
|
370
|
+
try {
|
|
371
|
+
await assertCodexAppServerHelperAccountIdentity(provider, input.expectedAccountIdentitySignature, helperOwnershipError);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
await helper.discard();
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
execution.validateOutput(result.text);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
await helper.discard();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
await helper.complete();
|
|
383
|
+
}
|
|
384
|
+
emitDetachedEvent({ status: result.status || "completed", text: result.text,
|
|
385
|
+
threadId, turnId: result.turnId || turnId, type: "completed" });
|
|
386
|
+
return { ok: true, text: result.text, threadId, turnId: result.turnId || turnId,
|
|
387
|
+
...(helperTurn ? { inputCharacters: prompt.length, outputCharacters: result.text.length, usage: result.usage || null } : {}) };
|
|
388
|
+
});
|
|
389
|
+
} finally {
|
|
390
|
+
admission.release();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
301
394
|
async function startCodexAppServerConversationTurn(sessionId, input = {}, options = {}) {
|
|
302
395
|
const messageId = normalizeCodexRunText(input.messageId);
|
|
303
396
|
return withCodexAppServerConversationTurnStart(sessionId, input, () => codexAppServerConversationResult(async () => {
|
|
@@ -1063,6 +1156,7 @@ export function createCodexConversationCommands({
|
|
|
1063
1156
|
codexAppServerConversationContext,
|
|
1064
1157
|
codexAppServerEphemeralScopeContext,
|
|
1065
1158
|
createCodexAppServerConversation,
|
|
1159
|
+
runDetachedCodexAppServerConversation,
|
|
1066
1160
|
startCodexAppServerConversationTurn,
|
|
1067
1161
|
readCodexAppServerConversation,
|
|
1068
1162
|
waitForCodexAppServerConversationTurn,
|