@kodaris/krubble-app-components 1.0.117 → 1.0.119
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/dist/chatbot.d.ts +32 -0
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +165 -8
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +166 -10
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +2 -3
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +166 -10
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +14 -15
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/dist/screen-nav.d.ts.map +1 -1
- package/dist/screen-nav.js +1 -2
- package/dist/screen-nav.js.map +1 -1
- package/package.json +1 -1
package/dist/krubble-app.umd.js
CHANGED
|
@@ -2484,8 +2484,7 @@
|
|
|
2484
2484
|
|
|
2485
2485
|
.nav {
|
|
2486
2486
|
width: 200px;
|
|
2487
|
-
background:
|
|
2488
|
-
border-right: 1px solid #e5e7eb;
|
|
2487
|
+
background: #f3f7fcbf;
|
|
2489
2488
|
display: flex;
|
|
2490
2489
|
flex-direction: column;
|
|
2491
2490
|
flex-shrink: 0;
|
|
@@ -5668,6 +5667,21 @@
|
|
|
5668
5667
|
this._reader = null;
|
|
5669
5668
|
this._decoder = new TextDecoder();
|
|
5670
5669
|
this._buffer = '';
|
|
5670
|
+
/**
|
|
5671
|
+
* True once the current turn ended cleanly (a `finished:true` chunk arrived).
|
|
5672
|
+
* Distinguishes a real completion from a dropped stream: if the SSE stream
|
|
5673
|
+
* closes (`result.done`) while this is still false, the turn is NOT over — the
|
|
5674
|
+
* connection dropped (5-min emitter timeout, tab was backgrounded, network
|
|
5675
|
+
* blip) — so we reconnect instead of unlocking the composer. Reset per turn.
|
|
5676
|
+
*/
|
|
5677
|
+
this._turnFinishedCleanly = false;
|
|
5678
|
+
/**
|
|
5679
|
+
* Consecutive failed reconnect attempts for the current drop. Bounds the retry
|
|
5680
|
+
* loop so a genuinely dead turn (server gone) can't spin forever — after the
|
|
5681
|
+
* cap we give up and finish the turn. Reset when a reconnect succeeds or a new
|
|
5682
|
+
* turn starts.
|
|
5683
|
+
*/
|
|
5684
|
+
this._reconnectAttempts = 0;
|
|
5671
5685
|
this._userHasScrolledUp = false;
|
|
5672
5686
|
this._isDragging = false;
|
|
5673
5687
|
this._isResizing = false;
|
|
@@ -6127,6 +6141,20 @@
|
|
|
6127
6141
|
data,
|
|
6128
6142
|
});
|
|
6129
6143
|
}
|
|
6144
|
+
// Persisted reasoning row — the DB stores the model's thinking as its own
|
|
6145
|
+
// row with the text in `content` and type "reasoning". Live, reasoning
|
|
6146
|
+
// instead arrives as REASONING_PART deltas accumulated onto an assistant
|
|
6147
|
+
// bubble's `reasoning` property, which is the ONLY shape the render shows
|
|
6148
|
+
// (via msg.reasoning). Map the DB row into that same shape — an assistant
|
|
6149
|
+
// bubble carrying the text in `reasoning` — so reloaded/reconciled
|
|
6150
|
+
// reasoning renders identically to live instead of vanishing.
|
|
6151
|
+
if (msg.type === 'reasoning') {
|
|
6152
|
+
return Promise.resolve({
|
|
6153
|
+
type: 'assistant',
|
|
6154
|
+
content: '',
|
|
6155
|
+
reasoning: msg.content || '',
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6130
6158
|
// A persisted approve/deny record — content is {outcome, tools:[...]}.
|
|
6131
6159
|
// Keep the type, parse content into `data`; the render reads msg.data.*.
|
|
6132
6160
|
if (msg.type === 'action_decision') {
|
|
@@ -6169,6 +6197,16 @@
|
|
|
6169
6197
|
if (messages && messages.length) {
|
|
6170
6198
|
this.messages = messages;
|
|
6171
6199
|
}
|
|
6200
|
+
// A turn may still be running server-side for this conversation (the page was
|
|
6201
|
+
// reloaded mid-turn, or opened on another device while a turn streams). After
|
|
6202
|
+
// the transcript loads, try to attach to that live turn: _reconnectStream hits
|
|
6203
|
+
// the read-only attach endpoint — 200 resumes streaming (locks the composer +
|
|
6204
|
+
// shows the typing indicator so the user knows the AI is still working), 204
|
|
6205
|
+
// means nothing is running (stays idle). Only meaningful with a pinned
|
|
6206
|
+
// conversation and the built-in stream (a custom `send` owns its own turns).
|
|
6207
|
+
if (this.conversationId && !this._isStreaming) {
|
|
6208
|
+
this._reconnectStream();
|
|
6209
|
+
}
|
|
6172
6210
|
}).catch(() => { });
|
|
6173
6211
|
// Fetch the framework config on load (debugTelemetryEnabled, v2ChatEnabled,
|
|
6174
6212
|
// registryServer). debugTelemetryEnabled gates the debug affordances and is
|
|
@@ -7208,6 +7246,10 @@
|
|
|
7208
7246
|
content: '',
|
|
7209
7247
|
}];
|
|
7210
7248
|
this._isStreaming = true;
|
|
7249
|
+
// Fresh turn: not yet finished, no reconnects used. These gate the drop-vs-done
|
|
7250
|
+
// decision in _readStreamChunk when the stream closes.
|
|
7251
|
+
this._turnFinishedCleanly = false;
|
|
7252
|
+
this._reconnectAttempts = 0;
|
|
7211
7253
|
// Scroll user's message to the top (Gemini-style). The runway (min-height =
|
|
7212
7254
|
// viewport) goes on the agent-turn WRAPPER, not an individual message — the
|
|
7213
7255
|
// whole turn (reasoning, tool lines, text, decision) streams inside that one
|
|
@@ -7418,6 +7460,10 @@
|
|
|
7418
7460
|
const btn = this._messagesEl?.querySelector(`.confirmation__btn[data-decision="${decision}"]`);
|
|
7419
7461
|
btn?.showLoading();
|
|
7420
7462
|
this._isStreaming = true;
|
|
7463
|
+
// Fresh turn (confirmation decision runs a model turn too): reset the
|
|
7464
|
+
// drop-vs-done gates used by _readStreamChunk.
|
|
7465
|
+
this._turnFinishedCleanly = false;
|
|
7466
|
+
this._reconnectAttempts = 0;
|
|
7421
7467
|
this._userHasScrolledUp = false;
|
|
7422
7468
|
this.send({
|
|
7423
7469
|
message: '',
|
|
@@ -7720,11 +7766,18 @@
|
|
|
7720
7766
|
}
|
|
7721
7767
|
this._reader.read().then((result) => {
|
|
7722
7768
|
if (result.done) {
|
|
7723
|
-
//
|
|
7724
|
-
//
|
|
7725
|
-
//
|
|
7726
|
-
//
|
|
7727
|
-
|
|
7769
|
+
// The stream closed. Two very different cases:
|
|
7770
|
+
// 1. A `finished:true` chunk already arrived (_turnFinishedCleanly) — the
|
|
7771
|
+
// turn is genuinely over; finishing was already handled below. Nothing to do.
|
|
7772
|
+
// 2. No finished chunk yet — the connection DROPPED mid-turn (5-min emitter
|
|
7773
|
+
// timeout, tab backgrounded, network blip) but the server-side turn is
|
|
7774
|
+
// still running. Do NOT unlock the composer (that's the old bug that made
|
|
7775
|
+
// the input look ready while the AI was still working). Reconnect to the
|
|
7776
|
+
// live stream and keep receiving events.
|
|
7777
|
+
if (this._turnFinishedCleanly) {
|
|
7778
|
+
return;
|
|
7779
|
+
}
|
|
7780
|
+
this._reconnectStream();
|
|
7728
7781
|
return;
|
|
7729
7782
|
}
|
|
7730
7783
|
this._buffer = this._buffer + this._decoder.decode(result.value, { stream: true });
|
|
@@ -7877,7 +7930,9 @@
|
|
|
7877
7930
|
}
|
|
7878
7931
|
if (data.finished) {
|
|
7879
7932
|
// Primary unlock signal: the turn is over and the server released
|
|
7880
|
-
// the turn lock. The SSE stream closes right after this chunk.
|
|
7933
|
+
// the turn lock. The SSE stream closes right after this chunk. Mark
|
|
7934
|
+
// it clean so the imminent `result.done` doesn't trigger a reconnect.
|
|
7935
|
+
this._turnFinishedCleanly = true;
|
|
7881
7936
|
this._finishStream();
|
|
7882
7937
|
}
|
|
7883
7938
|
}
|
|
@@ -7889,9 +7944,110 @@
|
|
|
7889
7944
|
}
|
|
7890
7945
|
this._readStreamChunk();
|
|
7891
7946
|
}).catch((err) => {
|
|
7892
|
-
if (err.name
|
|
7893
|
-
|
|
7947
|
+
if (err.name === 'AbortError') {
|
|
7948
|
+
return;
|
|
7949
|
+
}
|
|
7950
|
+
// A read error mid-turn is a drop, not a completion — reconnect (same as a
|
|
7951
|
+
// clean `done` without a finished chunk) unless the turn already finished.
|
|
7952
|
+
if (!this._turnFinishedCleanly) {
|
|
7953
|
+
this._reconnectStream();
|
|
7954
|
+
return;
|
|
7955
|
+
}
|
|
7956
|
+
this._handleStreamError(err.message);
|
|
7957
|
+
});
|
|
7958
|
+
}
|
|
7959
|
+
/**
|
|
7960
|
+
* Reconnect to an in-flight turn after the SSE stream dropped without finishing
|
|
7961
|
+
* (5-min emitter timeout, tab backgrounded, network blip). Hits the read-only
|
|
7962
|
+
* `/stream/attach/{conversationID}` endpoint, which re-attaches a fresh SSE
|
|
7963
|
+
* connection to the still-running server-side turn without starting anything new.
|
|
7964
|
+
*
|
|
7965
|
+
* - 200 with a body → resume reading; the turn's events keep flowing (events
|
|
7966
|
+
* emitted during the gap are not replayed, so the live text may jump forward —
|
|
7967
|
+
* the full transcript reconciles from the DB when the turn ends).
|
|
7968
|
+
* - 204 → no live turn: it already finished server-side while we were
|
|
7969
|
+
* disconnected. Reload the transcript so the final answer appears, then unlock.
|
|
7970
|
+
* - failure / retry cap hit → give up and finish so the composer never wedges.
|
|
7971
|
+
*
|
|
7972
|
+
* The composer stays locked (`_isStreaming` untouched) throughout, so the input
|
|
7973
|
+
* never looks ready while the AI is still working.
|
|
7974
|
+
*/
|
|
7975
|
+
_reconnectStream() {
|
|
7976
|
+
// Without a pinned conversation there's nothing to re-attach to (a custom
|
|
7977
|
+
// `send` owns its own targeting). Fall back to finishing.
|
|
7978
|
+
if (!this.conversationId) {
|
|
7979
|
+
this._finishStream();
|
|
7980
|
+
return;
|
|
7981
|
+
}
|
|
7982
|
+
// Bound the retries so a genuinely dead turn can't loop forever.
|
|
7983
|
+
if (this._reconnectAttempts >= 5) {
|
|
7984
|
+
this._handleStreamError('Lost connection to the assistant. Please reload the conversation.');
|
|
7985
|
+
return;
|
|
7986
|
+
}
|
|
7987
|
+
this._reconnectAttempts = this._reconnectAttempts + 1;
|
|
7988
|
+
krubbleHttp.Http.fetch({
|
|
7989
|
+
url: `/api/system/integration/aws/bedrock/stream/attach/${this.conversationId}`,
|
|
7990
|
+
method: 'GET',
|
|
7991
|
+
headers: { Accept: 'text/event-stream' },
|
|
7992
|
+
}).then((response) => {
|
|
7993
|
+
// 204 No Content: the turn finished server-side during the gap. Pull the
|
|
7994
|
+
// final transcript from the DB (via the same `load` callback the component
|
|
7995
|
+
// initializes with) so the answer that streamed during the disconnect
|
|
7996
|
+
// appears, then unlock.
|
|
7997
|
+
if (response.status === 204) {
|
|
7998
|
+
this._reconnectAttempts = 0;
|
|
7999
|
+
if (this.load) {
|
|
8000
|
+
this.load().then((messages) => {
|
|
8001
|
+
this.messages = messages;
|
|
8002
|
+
this.requestUpdate();
|
|
8003
|
+
}).catch(() => {
|
|
8004
|
+
// Reload failed — leave the transcript as-is; the user can refresh manually.
|
|
8005
|
+
});
|
|
8006
|
+
}
|
|
8007
|
+
this._finishStream();
|
|
8008
|
+
return;
|
|
8009
|
+
}
|
|
8010
|
+
if (!response.ok || !response.body) {
|
|
8011
|
+
// Transient failure — back off and try again on the same drop.
|
|
8012
|
+
setTimeout(() => this._reconnectStream(), 1000);
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
// Re-attached to the live turn: reset the retry counter and resume reading
|
|
8016
|
+
// into the same buffer/decoder from the fresh stream. Mark streaming so the
|
|
8017
|
+
// composer locks and the typing indicator shows — load-triggered attaches (a
|
|
8018
|
+
// page reloaded mid-turn) start with _isStreaming false, so this is what
|
|
8019
|
+
// surfaces "the AI is still working"; on a mid-session drop it's already true
|
|
8020
|
+
// (a no-op).
|
|
8021
|
+
this._reconnectAttempts = 0;
|
|
8022
|
+
this._isStreaming = true;
|
|
8023
|
+
this._reader = response.body.getReader();
|
|
8024
|
+
this._decoder = new TextDecoder();
|
|
8025
|
+
this._buffer = '';
|
|
8026
|
+
// Close the gap: events the turn emitted between the drop and this re-attach
|
|
8027
|
+
// aren't replayed on the live stream. Every COMPLETED iteration during the
|
|
8028
|
+
// gap was persisted (assistant text, tool_use/tool_result rows), so reload the
|
|
8029
|
+
// transcript from the DB to backfill them, THEN start reading live. Ordering
|
|
8030
|
+
// matters — the reader buffers incoming bytes until _readStreamChunk runs, so
|
|
8031
|
+
// starting live only after the async reload resolves prevents the reload from
|
|
8032
|
+
// clobbering freshly-streamed chunks. Only the current in-flight iteration's
|
|
8033
|
+
// partial text is missing from the reload; it streams live from here.
|
|
8034
|
+
if (this.load) {
|
|
8035
|
+
this.load().then((messages) => {
|
|
8036
|
+
this.messages = messages;
|
|
8037
|
+
this.requestUpdate();
|
|
8038
|
+
this._readStreamChunk();
|
|
8039
|
+
}).catch(() => {
|
|
8040
|
+
// Reload failed — go live anyway; the gap's completed iterations just
|
|
8041
|
+
// won't show until the next full reload, but the turn keeps streaming.
|
|
8042
|
+
this._readStreamChunk();
|
|
8043
|
+
});
|
|
8044
|
+
}
|
|
8045
|
+
else {
|
|
8046
|
+
this._readStreamChunk();
|
|
7894
8047
|
}
|
|
8048
|
+
}).catch(() => {
|
|
8049
|
+
// Network error reaching the attach endpoint — back off and retry.
|
|
8050
|
+
setTimeout(() => this._reconnectStream(), 1000);
|
|
7895
8051
|
});
|
|
7896
8052
|
}
|
|
7897
8053
|
/**
|