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