@cap-js/agents 0.9.6 → 0.9.8
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/README.md +2 -131
- package/_i18n/messages.properties +2 -0
- package/_i18n/messages_ar.properties +48 -0
- package/_i18n/messages_bg.properties +48 -0
- package/_i18n/messages_cs.properties +48 -0
- package/_i18n/messages_da.properties +48 -0
- package/_i18n/messages_de.properties +48 -0
- package/_i18n/messages_el.properties +48 -0
- package/_i18n/messages_en.properties +48 -0
- package/_i18n/messages_es.properties +48 -0
- package/_i18n/messages_es_MX.properties +48 -0
- package/_i18n/messages_fi.properties +48 -0
- package/_i18n/messages_fr.properties +48 -0
- package/_i18n/messages_he.properties +48 -0
- package/_i18n/messages_hr.properties +48 -0
- package/_i18n/messages_hu.properties +48 -0
- package/_i18n/messages_it.properties +48 -0
- package/_i18n/messages_ja.properties +48 -0
- package/_i18n/messages_kk.properties +48 -0
- package/_i18n/messages_ko.properties +48 -0
- package/_i18n/messages_ms.properties +48 -0
- package/_i18n/messages_nl.properties +48 -0
- package/_i18n/messages_no.properties +48 -0
- package/_i18n/messages_pl.properties +48 -0
- package/_i18n/messages_pt.properties +48 -0
- package/_i18n/messages_ro.properties +48 -0
- package/_i18n/messages_ru.properties +48 -0
- package/_i18n/messages_sh.properties +48 -0
- package/_i18n/messages_sk.properties +48 -0
- package/_i18n/messages_sl.properties +48 -0
- package/_i18n/messages_sv.properties +48 -0
- package/_i18n/messages_th.properties +48 -0
- package/_i18n/messages_tr.properties +48 -0
- package/_i18n/messages_uk.properties +48 -0
- package/_i18n/messages_vi.properties +48 -0
- package/_i18n/messages_zh_CN.properties +48 -0
- package/_i18n/messages_zh_TW.properties +48 -0
- package/cds-plugin.js +15 -10
- package/lib/agents/middleware/content-filter.js +4 -0
- package/lib/agents/middleware/hitl-decision-note-injector.js +18 -6
- package/lib/agents/middleware/hitl.js +17 -6
- package/lib/agents/middleware/index.js +3 -1
- package/lib/agents/middleware/masking.js +127 -0
- package/lib/agents/middleware/remote-mcp.js +10 -5
- package/lib/agents/middleware/status-update.js +209 -76
- package/lib/agents/middleware/tool-wrap.js +1 -2
- package/lib/agents/summarize-on-timeout.js +5 -2
- package/lib/config/local.js +121 -8
- package/lib/eval/eval-run.js +50 -4
- package/lib/masking/index.js +90 -0
- package/lib/masking/store.js +80 -0
- package/lib/masking/structured/findElements.js +459 -0
- package/lib/masking/structured/index.js +170 -0
- package/lib/masking/unstructured/dpi.js +84 -0
- package/lib/masking/unstructured/hana.js +72 -0
- package/lib/masking/unstructured/index.js +52 -0
- package/lib/models/aicore.js +16 -3
- package/lib/models/openai.js +9 -0
- package/lib/preview/chat.html +623 -119
- package/lib/protocol/persistence/cleanup.js +13 -4
- package/lib/telemetry/chat-tracing.js +9 -2
- package/lib/telemetry/mlflow/evaluation.js +83 -4
- package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +39 -5
- package/lib/telemetry/mlflow/exporter/MlflowExporter.js +43 -4
- package/lib/telemetry/mlflow/index.js +1 -7
- package/lib/telemetry/mlflow/prompts.js +15 -11
- package/lib/telemetry/mlflow/tracing.js +3 -3
- package/lib/telemetry/span-masking.js +165 -0
- package/lib/telemetry/tool-tracing.js +4 -4
- package/lib/utils/markdown.js +3 -10
- package/lib/utils/resilience.js +2 -1
- package/lib/utils/toml.js +67 -0
- package/lib/utils/usage.js +34 -0
- package/lib/utils/utils.js +43 -1
- package/package.json +33 -26
- package/srv/handlers/graph-executor/crash-handler.js +47 -0
- package/srv/handlers/graph-executor/hitl.js +27 -0
- package/srv/handlers/graph-executor.js +69 -20
- package/srv/handlers/index.js +6 -1
- package/srv/handlers/mcp-tools.js +11 -2
- package/srv/handlers/subagent-tools.js +13 -4
- package/srv/handlers/system-prompt.js +30 -20
- package/srv/handlers/tools.js +14 -13
package/lib/preview/chat.html
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
align-items: center;
|
|
27
27
|
gap: 12px;
|
|
28
28
|
flex-shrink: 0;
|
|
29
|
+
position: relative;
|
|
29
30
|
}
|
|
30
31
|
header h1 {
|
|
31
32
|
font-size: 1rem;
|
|
@@ -74,11 +75,15 @@
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
.msg,
|
|
77
|
-
.
|
|
78
|
+
.turn-panel {
|
|
78
79
|
max-width: 75%;
|
|
79
80
|
box-sizing: border-box;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
.msg-wrap .msg {
|
|
84
|
+
max-width: 100%;
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
.msg {
|
|
83
88
|
padding: 10px 14px;
|
|
84
89
|
border-radius: 12px;
|
|
@@ -96,6 +101,16 @@
|
|
|
96
101
|
border-bottom-right-radius: 3px;
|
|
97
102
|
white-space: pre-wrap;
|
|
98
103
|
}
|
|
104
|
+
.msg.user.queued {
|
|
105
|
+
opacity: 0.65;
|
|
106
|
+
}
|
|
107
|
+
.msg.user.queued::after {
|
|
108
|
+
content: "Queued";
|
|
109
|
+
display: block;
|
|
110
|
+
margin-top: 4px;
|
|
111
|
+
font-size: 0.7em;
|
|
112
|
+
text-align: right;
|
|
113
|
+
}
|
|
99
114
|
.msg.agent {
|
|
100
115
|
align-self: flex-start;
|
|
101
116
|
background: #d0e9ff;
|
|
@@ -246,6 +261,7 @@
|
|
|
246
261
|
|
|
247
262
|
#prompt {
|
|
248
263
|
flex: 1;
|
|
264
|
+
box-sizing: border-box;
|
|
249
265
|
padding: 10px 14px;
|
|
250
266
|
border: 1px solid #91adc5;
|
|
251
267
|
border-radius: 8px;
|
|
@@ -288,7 +304,6 @@
|
|
|
288
304
|
#send.cancel:hover {
|
|
289
305
|
background: #c62828;
|
|
290
306
|
}
|
|
291
|
-
|
|
292
307
|
.approval-actions {
|
|
293
308
|
display: flex;
|
|
294
309
|
gap: 8px;
|
|
@@ -400,7 +415,7 @@
|
|
|
400
415
|
|
|
401
416
|
/* Thinking messages are streamed into an expanded <details> panel,
|
|
402
417
|
then collapsed once the final response arrives. */
|
|
403
|
-
.
|
|
418
|
+
.turn-panel {
|
|
404
419
|
align-self: flex-start;
|
|
405
420
|
margin: 4px 0 8px 0;
|
|
406
421
|
font-size: 0.88em;
|
|
@@ -409,17 +424,17 @@
|
|
|
409
424
|
padding-left: 10px;
|
|
410
425
|
overflow-wrap: anywhere;
|
|
411
426
|
}
|
|
412
|
-
.
|
|
427
|
+
.turn-panel summary {
|
|
413
428
|
cursor: pointer;
|
|
414
429
|
user-select: none;
|
|
415
430
|
color: #888;
|
|
416
431
|
list-style: none;
|
|
417
432
|
}
|
|
418
|
-
.
|
|
433
|
+
.turn-panel > summary::before {
|
|
419
434
|
content: "▶ ";
|
|
420
435
|
font-size: 0.8em;
|
|
421
436
|
}
|
|
422
|
-
.
|
|
437
|
+
.turn-panel[open] > summary::before {
|
|
423
438
|
content: "▼ ";
|
|
424
439
|
}
|
|
425
440
|
.thinking-step {
|
|
@@ -428,27 +443,231 @@
|
|
|
428
443
|
font-style: italic;
|
|
429
444
|
overflow-x: auto;
|
|
430
445
|
}
|
|
431
|
-
.thinking-step
|
|
446
|
+
.thinking-step p {
|
|
447
|
+
margin: 0;
|
|
448
|
+
}
|
|
449
|
+
.thinking-step + .thinking-step,
|
|
450
|
+
.thinking-step + .tool-call-item,
|
|
451
|
+
.tool-call-item + .thinking-step,
|
|
452
|
+
.tool-call-item + .tool-call-item {
|
|
432
453
|
border-top: 1px dashed #e0e0e0;
|
|
433
454
|
margin-top: 4px;
|
|
434
455
|
}
|
|
435
456
|
@media (prefers-reduced-motion: no-preference) {
|
|
436
|
-
.
|
|
457
|
+
.turn-panel {
|
|
437
458
|
transition: height 0.16s ease;
|
|
438
459
|
}
|
|
439
460
|
}
|
|
440
461
|
@media (prefers-color-scheme: dark) {
|
|
441
|
-
.
|
|
462
|
+
.turn-panel {
|
|
442
463
|
color: #999;
|
|
443
464
|
border-left-color: #444;
|
|
444
465
|
}
|
|
445
|
-
.
|
|
466
|
+
.turn-panel summary {
|
|
446
467
|
color: #aaa;
|
|
447
468
|
}
|
|
448
|
-
.thinking-step + .thinking-step
|
|
469
|
+
.thinking-step + .thinking-step,
|
|
470
|
+
.thinking-step + .tool-call-item,
|
|
471
|
+
.tool-call-item + .thinking-step,
|
|
472
|
+
.tool-call-item + .tool-call-item {
|
|
449
473
|
border-top-color: #333;
|
|
450
474
|
}
|
|
451
475
|
}
|
|
476
|
+
.tool-call-item {
|
|
477
|
+
padding: 2px 0;
|
|
478
|
+
list-style: none;
|
|
479
|
+
}
|
|
480
|
+
.tool-call-item summary {
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
user-select: none;
|
|
483
|
+
list-style: none;
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: baseline;
|
|
486
|
+
gap: 4px;
|
|
487
|
+
padding: 2px 0;
|
|
488
|
+
}
|
|
489
|
+
.tool-call-item > summary::before {
|
|
490
|
+
content: "▶";
|
|
491
|
+
font-size: 0.7em;
|
|
492
|
+
color: #aaa;
|
|
493
|
+
flex-shrink: 0;
|
|
494
|
+
}
|
|
495
|
+
.tool-call-item[open] > summary::before {
|
|
496
|
+
content: "▼";
|
|
497
|
+
}
|
|
498
|
+
.tool-call-item:not([hasdetails]) > summary::before {
|
|
499
|
+
visibility: hidden;
|
|
500
|
+
}
|
|
501
|
+
.tool-call-status {
|
|
502
|
+
font-size: 0.85em;
|
|
503
|
+
}
|
|
504
|
+
.tool-call-label {
|
|
505
|
+
font-weight: 500;
|
|
506
|
+
}
|
|
507
|
+
.tool-call-kind {
|
|
508
|
+
font-size: 0.78em;
|
|
509
|
+
padding: 1px 5px;
|
|
510
|
+
border-radius: 8px;
|
|
511
|
+
background: #eee;
|
|
512
|
+
color: #555;
|
|
513
|
+
}
|
|
514
|
+
.tool-call-kind.kind-query {
|
|
515
|
+
background: #dbeafe;
|
|
516
|
+
color: #1d4ed8;
|
|
517
|
+
}
|
|
518
|
+
.tool-call-kind.kind-call {
|
|
519
|
+
background: #fef3c7;
|
|
520
|
+
color: #92400e;
|
|
521
|
+
}
|
|
522
|
+
.tool-call-kind.kind-agent {
|
|
523
|
+
background: #ede9fe;
|
|
524
|
+
color: #5b21b6;
|
|
525
|
+
}
|
|
526
|
+
.tool-call-detail {
|
|
527
|
+
margin: 4px 0 2px 12px;
|
|
528
|
+
padding: 4px 8px;
|
|
529
|
+
background: #f9f9f9;
|
|
530
|
+
border-radius: 4px;
|
|
531
|
+
font-family: monospace;
|
|
532
|
+
font-size: 0.85em;
|
|
533
|
+
white-space: pre-wrap;
|
|
534
|
+
overflow-x: auto;
|
|
535
|
+
max-height: 200px;
|
|
536
|
+
overflow-y: auto;
|
|
537
|
+
}
|
|
538
|
+
.tool-call-detail-label {
|
|
539
|
+
font-size: 0.75em;
|
|
540
|
+
color: #999;
|
|
541
|
+
margin: 6px 0 2px 12px;
|
|
542
|
+
text-transform: uppercase;
|
|
543
|
+
letter-spacing: 0.04em;
|
|
544
|
+
}
|
|
545
|
+
@media (prefers-color-scheme: dark) {
|
|
546
|
+
.tool-call-item > summary::before {
|
|
547
|
+
color: #555;
|
|
548
|
+
}
|
|
549
|
+
.tool-call-kind {
|
|
550
|
+
background: #333;
|
|
551
|
+
color: #aaa;
|
|
552
|
+
}
|
|
553
|
+
.tool-call-kind.kind-query {
|
|
554
|
+
background: #1e3a5f;
|
|
555
|
+
color: #93c5fd;
|
|
556
|
+
}
|
|
557
|
+
.tool-call-kind.kind-call {
|
|
558
|
+
background: #451a03;
|
|
559
|
+
color: #fcd34d;
|
|
560
|
+
}
|
|
561
|
+
.tool-call-kind.kind-agent {
|
|
562
|
+
background: #2e1065;
|
|
563
|
+
color: #c4b5fd;
|
|
564
|
+
}
|
|
565
|
+
.tool-call-detail {
|
|
566
|
+
background: #1a1a1a;
|
|
567
|
+
color: #ccc;
|
|
568
|
+
}
|
|
569
|
+
.tool-call-detail-label {
|
|
570
|
+
color: #555;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/* Subtle per-message metadata: context size + response time */
|
|
574
|
+
.msg-wrap {
|
|
575
|
+
display: flex;
|
|
576
|
+
flex-direction: column;
|
|
577
|
+
align-self: flex-start;
|
|
578
|
+
max-width: 75%;
|
|
579
|
+
}
|
|
580
|
+
.msg-meta {
|
|
581
|
+
display: inline-block;
|
|
582
|
+
align-self: flex-end;
|
|
583
|
+
margin-top: 3px;
|
|
584
|
+
font-size: 10px;
|
|
585
|
+
cursor: default;
|
|
586
|
+
position: relative;
|
|
587
|
+
white-space: nowrap;
|
|
588
|
+
color: rgba(83, 116, 146, 0.5);
|
|
589
|
+
}
|
|
590
|
+
.msg-meta:hover {
|
|
591
|
+
color: rgba(83, 116, 146, 0.85);
|
|
592
|
+
}
|
|
593
|
+
/* Hover tooltip with full token breakdown */
|
|
594
|
+
.msg-meta-tooltip {
|
|
595
|
+
display: none;
|
|
596
|
+
position: absolute;
|
|
597
|
+
bottom: calc(100% + 6px);
|
|
598
|
+
right: 0;
|
|
599
|
+
background: rgba(42, 61, 82, 0.72);
|
|
600
|
+
color: #e8f2fa;
|
|
601
|
+
font-size: 11px;
|
|
602
|
+
border-radius: 6px;
|
|
603
|
+
padding: 7px 9px;
|
|
604
|
+
z-index: 100;
|
|
605
|
+
pointer-events: none;
|
|
606
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
607
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
608
|
+
backdrop-filter: blur(8px);
|
|
609
|
+
-webkit-backdrop-filter: blur(8px);
|
|
610
|
+
text-align: left;
|
|
611
|
+
}
|
|
612
|
+
.msg-meta-tooltip-grid {
|
|
613
|
+
display: grid;
|
|
614
|
+
grid-template-columns: auto auto;
|
|
615
|
+
column-gap: 16px;
|
|
616
|
+
row-gap: 2px;
|
|
617
|
+
white-space: nowrap;
|
|
618
|
+
}
|
|
619
|
+
.msg-meta-tooltip-grid .tt-indent {
|
|
620
|
+
padding-left: 12px;
|
|
621
|
+
}
|
|
622
|
+
.msg-meta-tooltip-grid .tt-val {
|
|
623
|
+
text-align: right;
|
|
624
|
+
}
|
|
625
|
+
.msg-meta:hover .msg-meta-tooltip {
|
|
626
|
+
display: block;
|
|
627
|
+
}
|
|
628
|
+
/* Total context indicator: right-aligned row just above the input bar */
|
|
629
|
+
#context-indicator-row {
|
|
630
|
+
display: none;
|
|
631
|
+
padding: 2px 20px 2px;
|
|
632
|
+
text-align: right;
|
|
633
|
+
flex-shrink: 0;
|
|
634
|
+
}
|
|
635
|
+
#context-indicator-row.visible {
|
|
636
|
+
display: block;
|
|
637
|
+
}
|
|
638
|
+
#context-indicator {
|
|
639
|
+
font-size: 10px;
|
|
640
|
+
opacity: 0.45;
|
|
641
|
+
white-space: nowrap;
|
|
642
|
+
color: #537492;
|
|
643
|
+
position: relative;
|
|
644
|
+
cursor: default;
|
|
645
|
+
transition: opacity 0.15s;
|
|
646
|
+
}
|
|
647
|
+
#context-indicator:hover {
|
|
648
|
+
opacity: 0.85;
|
|
649
|
+
}
|
|
650
|
+
/* Hover tooltip mirroring per-message breakdown, anchored to the indicator */
|
|
651
|
+
#context-indicator:hover .msg-meta-tooltip {
|
|
652
|
+
display: block;
|
|
653
|
+
}
|
|
654
|
+
@media (prefers-color-scheme: dark) {
|
|
655
|
+
.msg-meta {
|
|
656
|
+
color: rgba(145, 173, 197, 0.55);
|
|
657
|
+
}
|
|
658
|
+
.msg-meta:hover {
|
|
659
|
+
color: rgba(145, 173, 197, 0.9);
|
|
660
|
+
}
|
|
661
|
+
.msg-meta-tooltip {
|
|
662
|
+
background: rgba(10, 30, 48, 0.72);
|
|
663
|
+
color: #c8dff0;
|
|
664
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
665
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
|
|
666
|
+
}
|
|
667
|
+
#context-indicator {
|
|
668
|
+
color: #91adc5;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
452
671
|
</style>
|
|
453
672
|
<script src="marked.min.js"></script>
|
|
454
673
|
</head>
|
|
@@ -464,6 +683,12 @@
|
|
|
464
683
|
<div class="status-text" id="status-text"></div>
|
|
465
684
|
</div>
|
|
466
685
|
|
|
686
|
+
<div id="context-indicator-row">
|
|
687
|
+
<span id="context-indicator"
|
|
688
|
+
><span class="msg-meta-tooltip" id="context-indicator-tooltip"></span
|
|
689
|
+
></span>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
467
692
|
<div id="input-row">
|
|
468
693
|
<textarea id="prompt" rows="1" placeholder="Message {{agentName}}…" autofocus></textarea>
|
|
469
694
|
<button id="send">Send</button>
|
|
@@ -508,21 +733,156 @@
|
|
|
508
733
|
const THINKING_SUMMARY_ANIMATION_MS = 160
|
|
509
734
|
|
|
510
735
|
let busy = false
|
|
736
|
+
let processingQueue = false
|
|
737
|
+
const messageQueue = []
|
|
511
738
|
let contextId = null
|
|
512
739
|
let pendingTaskId = null
|
|
513
740
|
let activeTaskId = null
|
|
514
741
|
let activeAbort = null
|
|
515
742
|
let streamingBubble = null
|
|
743
|
+
let lastStreamingBubble = null
|
|
516
744
|
let streamingText = ""
|
|
517
745
|
let thinkingFinalized = false
|
|
518
|
-
let thinkingStepEls = []
|
|
519
|
-
let thinkingTexts = []
|
|
520
|
-
let
|
|
521
|
-
let thinkingSummaryEl = null // <details> containing thinking steps
|
|
522
|
-
let currentThinkingArtifactId = null // artifactId of open thinking step
|
|
523
|
-
let currentThinkingClosed = false // true once lastChunk seen for current id
|
|
746
|
+
let thinkingStepEls = []
|
|
747
|
+
let thinkingTexts = []
|
|
748
|
+
let turnPanelEl = null // single <details> for thinking steps + tool calls
|
|
524
749
|
let pendingThinkingUpdate = null
|
|
525
750
|
let pendingThinkingTimer = null
|
|
751
|
+
let toolCallsMap = new Map() // tool call id → { el, data }
|
|
752
|
+
|
|
753
|
+
// Token accounting. The server sends cumulative token counts (a running sum
|
|
754
|
+
// over every model call in the conversation) plus context_tokens — the prompt
|
|
755
|
+
// size of the most recent model call, i.e. the current live context occupancy.
|
|
756
|
+
// Every field except context_tokens is cumulative, so per-turn values are
|
|
757
|
+
// derived by diffing against the previous turn.
|
|
758
|
+
let contextTokens = 0 // current live context size (last-call input incl. cache)
|
|
759
|
+
let prevContextTokens = 0 // context size after the previous turn
|
|
760
|
+
// Cumulative counts as of the previous turn, for per-turn deltas.
|
|
761
|
+
let prev = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0, reasoning: 0 }
|
|
762
|
+
let responseStartTime = null
|
|
763
|
+
const contextIndicator = document.getElementById("context-indicator")
|
|
764
|
+
const contextIndicatorTooltip = document.getElementById("context-indicator-tooltip")
|
|
765
|
+
|
|
766
|
+
function fmt(n) {
|
|
767
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"
|
|
768
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "k"
|
|
769
|
+
return String(n)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function fmtDuration(ms) {
|
|
773
|
+
if (ms < 1000) return ms + "ms"
|
|
774
|
+
return (ms / 1000).toFixed(1) + "s"
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function buildTooltipTable(rows) {
|
|
778
|
+
const grid = document.createElement("div")
|
|
779
|
+
grid.className = "msg-meta-tooltip-grid"
|
|
780
|
+
for (const row of rows) {
|
|
781
|
+
const label = document.createElement("div")
|
|
782
|
+
if (row.indent) label.className = "tt-indent"
|
|
783
|
+
label.textContent = row.label
|
|
784
|
+
const val = document.createElement("div")
|
|
785
|
+
val.className = "tt-val"
|
|
786
|
+
val.textContent = row.value
|
|
787
|
+
grid.appendChild(label)
|
|
788
|
+
grid.appendChild(val)
|
|
789
|
+
}
|
|
790
|
+
return grid
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function buildTokenRows(contextVal, input, cacheRead, cacheCreate, output, reasoning) {
|
|
794
|
+
const rows = [{ label: "Context", value: contextVal }]
|
|
795
|
+
if (input > 0) {
|
|
796
|
+
rows.push({ label: "Input", value: fmt(input) })
|
|
797
|
+
if (cacheRead > 0) rows.push({ label: "Cache read", value: fmt(cacheRead), indent: true })
|
|
798
|
+
if (cacheCreate > 0)
|
|
799
|
+
rows.push({ label: "Cache write", value: fmt(cacheCreate), indent: true })
|
|
800
|
+
}
|
|
801
|
+
if (output > 0)
|
|
802
|
+
rows.push({
|
|
803
|
+
label: "Output" + (reasoning > 0 ? " (reasoning " + fmt(reasoning) + ")" : ""),
|
|
804
|
+
value: fmt(output),
|
|
805
|
+
})
|
|
806
|
+
return rows
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function updateContextIndicator() {
|
|
810
|
+
if (contextTokens === 0) return
|
|
811
|
+
contextIndicator.childNodes[0].nodeType === Node.TEXT_NODE
|
|
812
|
+
? (contextIndicator.childNodes[0].textContent = "⛁ " + fmt(contextTokens))
|
|
813
|
+
: contextIndicator.insertBefore(
|
|
814
|
+
document.createTextNode("⛁ " + fmt(contextTokens)),
|
|
815
|
+
contextIndicatorTooltip,
|
|
816
|
+
)
|
|
817
|
+
contextIndicatorTooltip.innerHTML = ""
|
|
818
|
+
contextIndicatorTooltip.appendChild(
|
|
819
|
+
buildTooltipTable(
|
|
820
|
+
buildTokenRows(
|
|
821
|
+
fmt(contextTokens),
|
|
822
|
+
prev.input,
|
|
823
|
+
prev.cacheRead,
|
|
824
|
+
prev.cacheCreate,
|
|
825
|
+
prev.output,
|
|
826
|
+
prev.reasoning,
|
|
827
|
+
),
|
|
828
|
+
),
|
|
829
|
+
)
|
|
830
|
+
document.getElementById("context-indicator-row").classList.add("visible")
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function attachMsgMeta(el, usage, durationMs) {
|
|
834
|
+
// All these fields are cumulative across the conversation; diff against the
|
|
835
|
+
// previous turn to get this turn's billing-relevant amounts. input_tokens
|
|
836
|
+
// already includes cache-read/cache-write tokens, so the uncached (full-rate)
|
|
837
|
+
// input is input minus those.
|
|
838
|
+
const stepInTotal = (usage.input_tokens || 0) - prev.input
|
|
839
|
+
const stepOut = (usage.output_tokens || 0) - prev.output
|
|
840
|
+
const stepCacheRead = (usage.cache_read_input_tokens || 0) - prev.cacheRead
|
|
841
|
+
const stepCacheCreate = (usage.cache_creation_input_tokens || 0) - prev.cacheCreate
|
|
842
|
+
const stepReasoning = (usage.reasoning_tokens || 0) - prev.reasoning
|
|
843
|
+
const newContext = usage.context_tokens || usage.input_tokens || 0
|
|
844
|
+
|
|
845
|
+
// Context growth: how much this exchange added to the live context window.
|
|
846
|
+
const contextGrowth = newContext - prevContextTokens
|
|
847
|
+
|
|
848
|
+
prevContextTokens = newContext
|
|
849
|
+
contextTokens = newContext
|
|
850
|
+
prev = {
|
|
851
|
+
input: usage.input_tokens || 0,
|
|
852
|
+
output: usage.output_tokens || 0,
|
|
853
|
+
cacheRead: usage.cache_read_input_tokens || 0,
|
|
854
|
+
cacheCreate: usage.cache_creation_input_tokens || 0,
|
|
855
|
+
reasoning: usage.reasoning_tokens || 0,
|
|
856
|
+
}
|
|
857
|
+
updateContextIndicator()
|
|
858
|
+
|
|
859
|
+
const meta = document.createElement("span")
|
|
860
|
+
meta.className = "msg-meta"
|
|
861
|
+
|
|
862
|
+
const summary =
|
|
863
|
+
"⛁ " + fmt(contextGrowth) + (durationMs != null ? " · " + fmtDuration(durationMs) : "")
|
|
864
|
+
meta.appendChild(document.createTextNode(summary))
|
|
865
|
+
|
|
866
|
+
// Hover tooltip: per-turn, billing-relevant token breakdown.
|
|
867
|
+
const tooltip = document.createElement("span")
|
|
868
|
+
tooltip.className = "msg-meta-tooltip"
|
|
869
|
+
const growthLabel = (contextGrowth >= 0 ? "+" : "") + fmt(contextGrowth)
|
|
870
|
+
tooltip.appendChild(
|
|
871
|
+
buildTooltipTable(
|
|
872
|
+
buildTokenRows(
|
|
873
|
+
fmt(contextTokens) + " (" + growthLabel + ")",
|
|
874
|
+
stepInTotal,
|
|
875
|
+
stepCacheRead,
|
|
876
|
+
stepCacheCreate,
|
|
877
|
+
stepOut,
|
|
878
|
+
stepReasoning,
|
|
879
|
+
),
|
|
880
|
+
),
|
|
881
|
+
)
|
|
882
|
+
meta.appendChild(tooltip)
|
|
883
|
+
|
|
884
|
+
;(el._wrap ?? el).appendChild(meta)
|
|
885
|
+
}
|
|
526
886
|
|
|
527
887
|
// Auto-enable streaming when agent card advertises streaming capability
|
|
528
888
|
fetch(AGENT_URL + ".well-known/agent-card.json", { headers: authHeaders() })
|
|
@@ -547,13 +907,14 @@
|
|
|
547
907
|
|
|
548
908
|
prompt.addEventListener("input", () => {
|
|
549
909
|
prompt.style.height = "auto"
|
|
550
|
-
|
|
910
|
+
const borderHeight = prompt.offsetHeight - prompt.clientHeight
|
|
911
|
+
prompt.style.height = Math.min(prompt.scrollHeight + borderHeight, 140) + "px"
|
|
912
|
+
renderComposerAction()
|
|
551
913
|
})
|
|
552
914
|
prompt.addEventListener("keydown", (e) => {
|
|
553
915
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
554
916
|
e.preventDefault()
|
|
555
|
-
|
|
556
|
-
else send()
|
|
917
|
+
enqueueMessage()
|
|
557
918
|
} else if (e.key === "ArrowUp" && prompt.selectionStart === 0) {
|
|
558
919
|
e.preventDefault()
|
|
559
920
|
if (historyIndex === -1) currentDraft = prompt.value
|
|
@@ -561,6 +922,7 @@
|
|
|
561
922
|
historyIndex++
|
|
562
923
|
prompt.value = inputHistory[inputHistory.length - 1 - historyIndex]
|
|
563
924
|
prompt.selectionStart = prompt.selectionEnd = prompt.value.length
|
|
925
|
+
renderComposerAction()
|
|
564
926
|
}
|
|
565
927
|
} else if (e.key === "ArrowDown" && prompt.selectionEnd === prompt.value.length) {
|
|
566
928
|
e.preventDefault()
|
|
@@ -568,55 +930,65 @@
|
|
|
568
930
|
historyIndex--
|
|
569
931
|
prompt.value = inputHistory[inputHistory.length - 1 - historyIndex]
|
|
570
932
|
prompt.selectionStart = prompt.selectionEnd = prompt.value.length
|
|
933
|
+
renderComposerAction()
|
|
571
934
|
} else if (historyIndex === 0) {
|
|
572
935
|
historyIndex = -1
|
|
573
936
|
prompt.value = currentDraft
|
|
574
937
|
prompt.selectionStart = prompt.selectionEnd = prompt.value.length
|
|
938
|
+
renderComposerAction()
|
|
575
939
|
}
|
|
576
940
|
}
|
|
577
941
|
})
|
|
578
942
|
sendBtn.addEventListener("click", () => {
|
|
579
|
-
if (busy) cancel()
|
|
580
|
-
else
|
|
943
|
+
if (busy && !prompt.value.trim()) cancel()
|
|
944
|
+
else enqueueMessage()
|
|
581
945
|
})
|
|
946
|
+
renderComposerAction()
|
|
582
947
|
|
|
583
|
-
function addMessage(role, text) {
|
|
948
|
+
function addMessage(role, text, beforeEl) {
|
|
584
949
|
const el = document.createElement("div")
|
|
585
950
|
el.className = "msg " + role
|
|
586
951
|
if (role === "agent" || role === "agent_thinking")
|
|
587
952
|
el.innerHTML = md(text, { sanitize: true })
|
|
588
953
|
else el.textContent = text
|
|
589
954
|
if (role === "agent_thinking") el.className = "msg agent_thinking"
|
|
590
|
-
|
|
955
|
+
const anchor = beforeEl ?? (typing.parentElement === messages ? typing : statusText)
|
|
956
|
+
if (role === "agent") {
|
|
957
|
+
const wrap = document.createElement("div")
|
|
958
|
+
wrap.className = "msg-wrap"
|
|
959
|
+
wrap.appendChild(el)
|
|
960
|
+
el._wrap = wrap
|
|
961
|
+
messages.insertBefore(wrap, anchor)
|
|
962
|
+
} else {
|
|
963
|
+
messages.insertBefore(el, anchor)
|
|
964
|
+
}
|
|
591
965
|
messages.scrollTop = messages.scrollHeight
|
|
592
966
|
return el
|
|
593
967
|
}
|
|
594
968
|
|
|
595
|
-
function
|
|
596
|
-
if (!
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
const count = thinkingStepCount
|
|
600
|
-
summary.textContent = `Thinking (${count} step${count === 1 ? "" : "s"})`
|
|
969
|
+
function updateTurnPanelLabel() {
|
|
970
|
+
if (!turnPanelEl) return
|
|
971
|
+
const n = thinkingStepEls.length + toolCallsMap.size
|
|
972
|
+
turnPanelEl.querySelector("summary").textContent = `Steps (${n})`
|
|
601
973
|
}
|
|
602
974
|
|
|
603
|
-
function
|
|
604
|
-
if (!
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
975
|
+
function ensureTurnPanel() {
|
|
976
|
+
if (!turnPanelEl) {
|
|
977
|
+
turnPanelEl = document.createElement("details")
|
|
978
|
+
turnPanelEl.className = "turn-panel"
|
|
979
|
+
turnPanelEl.open = true
|
|
608
980
|
const summary = document.createElement("summary")
|
|
609
|
-
summary.addEventListener("click",
|
|
610
|
-
|
|
611
|
-
updateThinkingSummaryLabel()
|
|
981
|
+
summary.addEventListener("click", toggleTurnPanel)
|
|
982
|
+
turnPanelEl.appendChild(summary)
|
|
612
983
|
messages.insertBefore(
|
|
613
|
-
|
|
984
|
+
turnPanelEl,
|
|
614
985
|
typing.parentElement === messages ? typing : statusText,
|
|
615
986
|
)
|
|
616
987
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
988
|
+
cancelTurnPanelAnimation(turnPanelEl)
|
|
989
|
+
turnPanelEl.open = true
|
|
990
|
+
updateTurnPanelLabel()
|
|
991
|
+
return turnPanelEl
|
|
620
992
|
}
|
|
621
993
|
|
|
622
994
|
function clearPendingThinkingUpdate() {
|
|
@@ -635,34 +1007,31 @@
|
|
|
635
1007
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
636
1008
|
}
|
|
637
1009
|
|
|
638
|
-
function
|
|
1010
|
+
function resetTurnPanelAnimation(el) {
|
|
639
1011
|
el.style.height = ""
|
|
640
1012
|
el.style.overflow = ""
|
|
641
1013
|
}
|
|
642
1014
|
|
|
643
|
-
function
|
|
1015
|
+
function cancelTurnPanelAnimation(el) {
|
|
644
1016
|
if (!el) return
|
|
645
|
-
el.
|
|
646
|
-
|
|
1017
|
+
el._animationId = (el._animationId ?? 0) + 1
|
|
1018
|
+
resetTurnPanelAnimation(el)
|
|
647
1019
|
}
|
|
648
1020
|
|
|
649
|
-
function
|
|
1021
|
+
function toggleTurnPanel(event) {
|
|
650
1022
|
if (prefersReducedMotion()) return
|
|
651
1023
|
event.preventDefault()
|
|
652
1024
|
const el = event.currentTarget.parentElement
|
|
653
|
-
if (el.open)
|
|
654
|
-
else
|
|
1025
|
+
if (el.open) collapseTurnPanel(el)
|
|
1026
|
+
else expandTurnPanel(el)
|
|
655
1027
|
}
|
|
656
1028
|
|
|
657
1029
|
function renderThinkingStep(update) {
|
|
658
|
-
const { artifactId, text,
|
|
659
|
-
const panel =
|
|
660
|
-
const idx = thinkingStepEls.findIndex((
|
|
661
|
-
if (
|
|
662
|
-
thinkingTexts[idx] += text
|
|
663
|
-
thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
|
|
664
|
-
} else if (artifactId === currentThinkingArtifactId && !currentThinkingClosed) {
|
|
665
|
-
thinkingTexts[idx] += "\n" + text
|
|
1030
|
+
const { artifactId, text, append } = update
|
|
1031
|
+
const panel = ensureTurnPanel()
|
|
1032
|
+
const idx = thinkingStepEls.findIndex((s) => s._artifactId === artifactId)
|
|
1033
|
+
if (idx !== -1) {
|
|
1034
|
+
thinkingTexts[idx] += append ? text : "\n" + text
|
|
666
1035
|
thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
|
|
667
1036
|
} else {
|
|
668
1037
|
const step = document.createElement("div")
|
|
@@ -672,12 +1041,8 @@
|
|
|
672
1041
|
panel.appendChild(step)
|
|
673
1042
|
thinkingStepEls.push(step)
|
|
674
1043
|
thinkingTexts.push(text)
|
|
675
|
-
|
|
676
|
-
updateThinkingSummaryLabel()
|
|
677
|
-
currentThinkingArtifactId = artifactId
|
|
678
|
-
currentThinkingClosed = false
|
|
1044
|
+
updateTurnPanelLabel()
|
|
679
1045
|
}
|
|
680
|
-
if (lastChunk) currentThinkingClosed = true
|
|
681
1046
|
messages.scrollTop = messages.scrollHeight
|
|
682
1047
|
}
|
|
683
1048
|
|
|
@@ -706,24 +1071,112 @@
|
|
|
706
1071
|
)
|
|
707
1072
|
}
|
|
708
1073
|
|
|
1074
|
+
function escHtml(str) {
|
|
1075
|
+
return String(str)
|
|
1076
|
+
.replace(/&/g, "&")
|
|
1077
|
+
.replace(/</g, "<")
|
|
1078
|
+
.replace(/>/g, ">")
|
|
1079
|
+
.replace(/"/g, """)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const TOOL_STATUS_ICON = { running: "⏳", done: "✓", error: "✗" }
|
|
1083
|
+
const TOOL_RESULT_TRUNCATE = 500
|
|
1084
|
+
|
|
1085
|
+
function updateToolCall(id, data) {
|
|
1086
|
+
let entry = toolCallsMap.get(id)
|
|
1087
|
+
if (!entry) {
|
|
1088
|
+
const el = document.createElement("details")
|
|
1089
|
+
el.className = "tool-call-item"
|
|
1090
|
+
el.innerHTML =
|
|
1091
|
+
`<summary>` +
|
|
1092
|
+
`<span class="tool-call-kind" hidden></span>` +
|
|
1093
|
+
`<span class="tool-call-label"></span>` +
|
|
1094
|
+
`<span class="tool-call-status"></span> ` +
|
|
1095
|
+
`</summary>` +
|
|
1096
|
+
`<div class="tool-call-detail-label" hidden>Args</div><pre class="tool-call-detail args" hidden></pre>` +
|
|
1097
|
+
`<div class="tool-call-detail-label" hidden>Result</div><pre class="tool-call-detail result" hidden></pre>`
|
|
1098
|
+
entry = { el, data: {} }
|
|
1099
|
+
toolCallsMap.set(id, entry)
|
|
1100
|
+
ensureTurnPanel().appendChild(el)
|
|
1101
|
+
} else {
|
|
1102
|
+
ensureTurnPanel()
|
|
1103
|
+
}
|
|
1104
|
+
Object.assign(entry.data, data)
|
|
1105
|
+
const { el } = entry
|
|
1106
|
+
const d = entry.data
|
|
1107
|
+
|
|
1108
|
+
el.querySelector(".tool-call-status").textContent = TOOL_STATUS_ICON[d.status] ?? "⏳"
|
|
1109
|
+
el.querySelector(".tool-call-label").textContent = d.label ?? d.name ?? ""
|
|
1110
|
+
if (d.kind) {
|
|
1111
|
+
const kindEl = el.querySelector(".tool-call-kind")
|
|
1112
|
+
kindEl.textContent = d.kind
|
|
1113
|
+
kindEl.className = `tool-call-kind kind-${d.kind}`
|
|
1114
|
+
kindEl.hidden = false
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const argsStr =
|
|
1118
|
+
d.args != null
|
|
1119
|
+
? typeof d.args === "string"
|
|
1120
|
+
? d.args
|
|
1121
|
+
: JSON.stringify(d.args, null, 2)
|
|
1122
|
+
: null
|
|
1123
|
+
let resultStr = null
|
|
1124
|
+
if (d.result != null) {
|
|
1125
|
+
const raw = typeof d.result === "string" ? d.result : JSON.stringify(d.result, null, 2)
|
|
1126
|
+
resultStr =
|
|
1127
|
+
raw.length > TOOL_RESULT_TRUNCATE
|
|
1128
|
+
? raw.slice(0, TOOL_RESULT_TRUNCATE) +
|
|
1129
|
+
`\n… (+${raw.length - TOOL_RESULT_TRUNCATE} chars)`
|
|
1130
|
+
: raw
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (argsStr != null) {
|
|
1134
|
+
const pre = el.querySelector(".args")
|
|
1135
|
+
pre.textContent = argsStr
|
|
1136
|
+
pre.hidden = false
|
|
1137
|
+
pre.previousElementSibling.hidden = false
|
|
1138
|
+
el.setAttribute("hasdetails", "")
|
|
1139
|
+
}
|
|
1140
|
+
if (resultStr != null) {
|
|
1141
|
+
const pre = el.querySelector(".result")
|
|
1142
|
+
pre.textContent = resultStr
|
|
1143
|
+
pre.hidden = false
|
|
1144
|
+
pre.previousElementSibling.hidden = false
|
|
1145
|
+
el.setAttribute("hasdetails", "")
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
messages.scrollTop = messages.scrollHeight
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function finalizeTurnPanel() {
|
|
1152
|
+
if (turnPanelEl?.open) {
|
|
1153
|
+
setTimeout(() => {
|
|
1154
|
+
if (turnPanelEl) collapseTurnPanel(turnPanelEl)
|
|
1155
|
+
}, 800)
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
709
1159
|
function promoteLatestThinkingToResponse(responseText) {
|
|
710
|
-
if (!
|
|
1160
|
+
if (!turnPanelEl || !latestThinkingMatches(responseText)) return null
|
|
711
1161
|
|
|
712
1162
|
thinkingTexts.pop()
|
|
713
1163
|
const responseEl = thinkingStepEls.pop()
|
|
714
|
-
thinkingStepCount = thinkingStepEls.length
|
|
715
1164
|
|
|
716
1165
|
responseEl.className = "msg agent"
|
|
717
1166
|
responseEl._promotedFromThinking = true
|
|
718
1167
|
responseEl.innerHTML = md(responseText)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1168
|
+
const wrap = document.createElement("div")
|
|
1169
|
+
wrap.className = "msg-wrap"
|
|
1170
|
+
wrap.appendChild(responseEl)
|
|
1171
|
+
responseEl._wrap = wrap
|
|
1172
|
+
messages.insertBefore(wrap, typing.parentElement === messages ? typing : statusText)
|
|
1173
|
+
|
|
1174
|
+
if (thinkingStepEls.length === 0 && toolCallsMap.size === 0) {
|
|
1175
|
+
turnPanelEl.remove()
|
|
1176
|
+
turnPanelEl = null
|
|
724
1177
|
} else {
|
|
725
|
-
|
|
726
|
-
|
|
1178
|
+
updateTurnPanelLabel()
|
|
1179
|
+
collapseTurnPanel(turnPanelEl)
|
|
727
1180
|
}
|
|
728
1181
|
|
|
729
1182
|
messages.scrollTop = messages.scrollHeight
|
|
@@ -733,28 +1186,27 @@
|
|
|
733
1186
|
function finalizeThinking(beforeEl, responseText) {
|
|
734
1187
|
clearPendingThinkingUpdate()
|
|
735
1188
|
if (beforeEl?._promotedFromThinking) return
|
|
736
|
-
if (thinkingStepEls.length === 0 || !
|
|
1189
|
+
if (thinkingStepEls.length === 0 || !turnPanelEl) return
|
|
737
1190
|
// Stream labels every model turn as thinking until terminal response is
|
|
738
1191
|
// known. Its final turn is authoritative response, never a thinking step.
|
|
739
1192
|
thinkingTexts.pop()
|
|
740
1193
|
const finalResponse = thinkingStepEls.pop()
|
|
741
1194
|
finalResponse?.remove()
|
|
742
|
-
|
|
743
|
-
updateThinkingSummaryLabel()
|
|
1195
|
+
updateTurnPanelLabel()
|
|
744
1196
|
|
|
745
|
-
if (thinkingStepEls.length === 0) {
|
|
746
|
-
|
|
747
|
-
|
|
1197
|
+
if (thinkingStepEls.length === 0 && toolCallsMap.size === 0) {
|
|
1198
|
+
turnPanelEl.remove()
|
|
1199
|
+
turnPanelEl = null
|
|
748
1200
|
return
|
|
749
1201
|
}
|
|
750
1202
|
|
|
751
|
-
if (beforeEl &&
|
|
752
|
-
messages.insertBefore(
|
|
1203
|
+
if (beforeEl && turnPanelEl.nextSibling !== (beforeEl._wrap ?? beforeEl)) {
|
|
1204
|
+
messages.insertBefore(turnPanelEl, beforeEl._wrap ?? beforeEl)
|
|
753
1205
|
}
|
|
754
|
-
|
|
1206
|
+
collapseTurnPanel(turnPanelEl)
|
|
755
1207
|
}
|
|
756
1208
|
|
|
757
|
-
function
|
|
1209
|
+
function collapseTurnPanel(el) {
|
|
758
1210
|
if (!el || !el.open) return
|
|
759
1211
|
|
|
760
1212
|
if (prefersReducedMotion()) {
|
|
@@ -765,20 +1217,20 @@
|
|
|
765
1217
|
const summary = el.querySelector("summary")
|
|
766
1218
|
const startHeight = `${el.offsetHeight}px`
|
|
767
1219
|
const endHeight = `${summary?.offsetHeight ?? 0}px`
|
|
768
|
-
const animationId = (el.
|
|
769
|
-
el.
|
|
1220
|
+
const animationId = (el._animationId ?? 0) + 1
|
|
1221
|
+
el._animationId = animationId
|
|
770
1222
|
let finished = false
|
|
771
1223
|
|
|
772
1224
|
const finish = () => {
|
|
773
1225
|
el.removeEventListener("transitionend", finish)
|
|
774
|
-
if (el.
|
|
1226
|
+
if (el._animationId !== animationId) return
|
|
775
1227
|
if (finished) return
|
|
776
1228
|
finished = true
|
|
777
1229
|
el.open = false
|
|
778
|
-
|
|
1230
|
+
resetTurnPanelAnimation(el)
|
|
779
1231
|
}
|
|
780
1232
|
|
|
781
|
-
|
|
1233
|
+
resetTurnPanelAnimation(el)
|
|
782
1234
|
el.style.overflow = "hidden"
|
|
783
1235
|
el.style.height = startHeight
|
|
784
1236
|
requestAnimationFrame(() => {
|
|
@@ -788,7 +1240,7 @@
|
|
|
788
1240
|
setTimeout(finish, THINKING_SUMMARY_ANIMATION_MS + 80)
|
|
789
1241
|
}
|
|
790
1242
|
|
|
791
|
-
function
|
|
1243
|
+
function expandTurnPanel(el) {
|
|
792
1244
|
if (!el || el.open) return
|
|
793
1245
|
|
|
794
1246
|
if (prefersReducedMotion()) {
|
|
@@ -800,19 +1252,19 @@
|
|
|
800
1252
|
const summary = el.querySelector("summary")
|
|
801
1253
|
const startHeight = `${summary?.offsetHeight ?? 0}px`
|
|
802
1254
|
const endHeight = `${el.scrollHeight}px`
|
|
803
|
-
const animationId = (el.
|
|
804
|
-
el.
|
|
1255
|
+
const animationId = (el._animationId ?? 0) + 1
|
|
1256
|
+
el._animationId = animationId
|
|
805
1257
|
let finished = false
|
|
806
1258
|
|
|
807
1259
|
const finish = () => {
|
|
808
1260
|
el.removeEventListener("transitionend", finish)
|
|
809
|
-
if (el.
|
|
1261
|
+
if (el._animationId !== animationId) return
|
|
810
1262
|
if (finished) return
|
|
811
1263
|
finished = true
|
|
812
|
-
|
|
1264
|
+
resetTurnPanelAnimation(el)
|
|
813
1265
|
}
|
|
814
1266
|
|
|
815
|
-
|
|
1267
|
+
resetTurnPanelAnimation(el)
|
|
816
1268
|
el.style.overflow = "hidden"
|
|
817
1269
|
el.style.height = startHeight
|
|
818
1270
|
requestAnimationFrame(() => {
|
|
@@ -869,31 +1321,34 @@
|
|
|
869
1321
|
|
|
870
1322
|
function setBusy(val) {
|
|
871
1323
|
busy = val
|
|
872
|
-
prompt.disabled = val
|
|
873
1324
|
if (typing.parentElement !== messages) messages.insertBefore(typing, statusText)
|
|
874
1325
|
typing.style.display = val ? "block" : "none"
|
|
875
1326
|
dot.className = "status-dot" + (val ? " busy" : "")
|
|
876
|
-
if (val) {
|
|
877
|
-
sendBtn.textContent = "Cancel"
|
|
878
|
-
sendBtn.classList.add("cancel")
|
|
879
|
-
sendBtn.disabled = false
|
|
880
|
-
} else {
|
|
881
|
-
sendBtn.textContent = "Send"
|
|
882
|
-
sendBtn.classList.remove("cancel")
|
|
883
|
-
sendBtn.disabled = false
|
|
1327
|
+
if (!val) {
|
|
884
1328
|
activeTaskId = null
|
|
885
1329
|
activeAbort = null
|
|
886
1330
|
statusText.textContent = ""
|
|
887
1331
|
}
|
|
1332
|
+
renderComposerAction()
|
|
888
1333
|
messages.scrollTop = messages.scrollHeight
|
|
889
1334
|
}
|
|
890
1335
|
|
|
1336
|
+
function renderComposerAction() {
|
|
1337
|
+
const stopsGeneration = busy && !prompt.value.trim()
|
|
1338
|
+
sendBtn.textContent = stopsGeneration ? "Cancel" : "Send"
|
|
1339
|
+
sendBtn.classList.toggle("cancel", stopsGeneration)
|
|
1340
|
+
sendBtn.setAttribute("aria-label", stopsGeneration ? "Stop generating" : "Send message")
|
|
1341
|
+
sendBtn.title = stopsGeneration ? "Stop generating" : "Send message"
|
|
1342
|
+
sendBtn.disabled = !busy && !prompt.value.trim()
|
|
1343
|
+
}
|
|
1344
|
+
|
|
891
1345
|
async function resume(decision) {
|
|
892
1346
|
const taskId = pendingTaskId
|
|
893
1347
|
pendingTaskId = null
|
|
894
1348
|
|
|
895
1349
|
activeTaskId = taskId
|
|
896
1350
|
setBusy(true)
|
|
1351
|
+
responseStartTime = performance.now()
|
|
897
1352
|
|
|
898
1353
|
const abort = new AbortController()
|
|
899
1354
|
activeAbort = abort
|
|
@@ -914,6 +1369,7 @@
|
|
|
914
1369
|
taskId,
|
|
915
1370
|
contextId,
|
|
916
1371
|
parts: [{ kind: "text", text: decision }],
|
|
1372
|
+
metadata: { "tool-status-update": { args: true, result: true } },
|
|
917
1373
|
},
|
|
918
1374
|
},
|
|
919
1375
|
}),
|
|
@@ -934,6 +1390,7 @@
|
|
|
934
1390
|
} finally {
|
|
935
1391
|
setBusy(false)
|
|
936
1392
|
prompt.focus()
|
|
1393
|
+
processQueue()
|
|
937
1394
|
}
|
|
938
1395
|
}
|
|
939
1396
|
|
|
@@ -957,9 +1414,19 @@
|
|
|
957
1414
|
return
|
|
958
1415
|
}
|
|
959
1416
|
|
|
1417
|
+
// Token usage: non-streaming embeds in status message metadata; streaming passes via _tokenUsage
|
|
1418
|
+
const tokenUsage =
|
|
1419
|
+
result?._tokenUsage ?? result?.status?.message?.metadata?.["sap.cds.agents.token-usage"]
|
|
1420
|
+
const durationMs =
|
|
1421
|
+
responseStartTime != null ? Math.round(performance.now() - responseStartTime) : null
|
|
1422
|
+
|
|
960
1423
|
// If streaming already rendered the final text via lastChunk, skip.
|
|
961
1424
|
if (useStreaming && streamingText !== "") {
|
|
962
|
-
if (!thinkingFinalized) finalizeThinking(
|
|
1425
|
+
if (!thinkingFinalized) finalizeThinking(lastStreamingBubble, streamingText)
|
|
1426
|
+
if (tokenUsage && lastStreamingBubble)
|
|
1427
|
+
attachMsgMeta(lastStreamingBubble, tokenUsage, durationMs)
|
|
1428
|
+
finalizeTurnPanel()
|
|
1429
|
+
lastStreamingBubble = null
|
|
963
1430
|
streamingBubble = null
|
|
964
1431
|
streamingText = ""
|
|
965
1432
|
return
|
|
@@ -969,6 +1436,7 @@
|
|
|
969
1436
|
if (streamingBubble) {
|
|
970
1437
|
if (streamingText) streamingBubble.innerHTML = md(streamingText)
|
|
971
1438
|
finalizeThinking(streamingBubble, streamingText)
|
|
1439
|
+
finalizeTurnPanel()
|
|
972
1440
|
streamingBubble = null
|
|
973
1441
|
streamingText = ""
|
|
974
1442
|
return
|
|
@@ -979,14 +1447,15 @@
|
|
|
979
1447
|
const reply = partsToText(parts)
|
|
980
1448
|
const replyEl =
|
|
981
1449
|
promoteLatestThinkingToResponse(reply) || addMessage("agent", reply || "(no response)")
|
|
1450
|
+
if (tokenUsage) attachMsgMeta(replyEl, tokenUsage, durationMs)
|
|
982
1451
|
finalizeThinking(replyEl, reply)
|
|
1452
|
+
finalizeTurnPanel()
|
|
983
1453
|
}
|
|
984
1454
|
|
|
985
|
-
|
|
1455
|
+
function enqueueMessage() {
|
|
986
1456
|
const text = prompt.value.trim()
|
|
987
|
-
if (!text
|
|
1457
|
+
if (!text) return
|
|
988
1458
|
|
|
989
|
-
// Save to input history (skip if same as last entry)
|
|
990
1459
|
if (inputHistory[inputHistory.length - 1] !== text) {
|
|
991
1460
|
inputHistory.push(text)
|
|
992
1461
|
if (inputHistory.length > 100) inputHistory.shift()
|
|
@@ -994,24 +1463,45 @@
|
|
|
994
1463
|
}
|
|
995
1464
|
historyIndex = -1
|
|
996
1465
|
currentDraft = ""
|
|
997
|
-
|
|
998
|
-
addMessage("user", text)
|
|
999
1466
|
prompt.value = ""
|
|
1000
1467
|
prompt.style.height = "auto"
|
|
1468
|
+
renderComposerAction()
|
|
1469
|
+
const el = addMessage("user", text, statusText)
|
|
1470
|
+
el.classList.add("queued")
|
|
1471
|
+
messageQueue.push({ text, el })
|
|
1472
|
+
processQueue()
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
async function processQueue() {
|
|
1476
|
+
if (processingQueue || pendingTaskId) return
|
|
1477
|
+
processingQueue = true
|
|
1478
|
+
try {
|
|
1479
|
+
while (messageQueue.length && !pendingTaskId) {
|
|
1480
|
+
const message = messageQueue.shift()
|
|
1481
|
+
await sendMessage(message)
|
|
1482
|
+
}
|
|
1483
|
+
} finally {
|
|
1484
|
+
processingQueue = false
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function sendMessage({ text, el }) {
|
|
1489
|
+
el.classList.remove("queued")
|
|
1490
|
+
messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
|
|
1001
1491
|
setBusy(true)
|
|
1492
|
+
responseStartTime = performance.now()
|
|
1002
1493
|
|
|
1003
1494
|
const abort = new AbortController()
|
|
1004
1495
|
activeAbort = abort
|
|
1005
1496
|
streamingBubble = null
|
|
1497
|
+
lastStreamingBubble = null
|
|
1006
1498
|
streamingText = ""
|
|
1007
1499
|
thinkingFinalized = false
|
|
1008
1500
|
clearPendingThinkingUpdate()
|
|
1009
1501
|
thinkingStepEls = []
|
|
1010
1502
|
thinkingTexts = []
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
currentThinkingArtifactId = null
|
|
1014
|
-
currentThinkingClosed = false
|
|
1503
|
+
turnPanelEl = null
|
|
1504
|
+
toolCallsMap = new Map()
|
|
1015
1505
|
|
|
1016
1506
|
const useStream = useStreaming
|
|
1017
1507
|
try {
|
|
@@ -1029,6 +1519,12 @@
|
|
|
1029
1519
|
messageId: crypto.randomUUID(),
|
|
1030
1520
|
...(contextId ? { contextId } : {}),
|
|
1031
1521
|
parts: [{ kind: "text", text }],
|
|
1522
|
+
metadata: {
|
|
1523
|
+
"tool-status-update": {
|
|
1524
|
+
args: true,
|
|
1525
|
+
result: true,
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1032
1528
|
},
|
|
1033
1529
|
},
|
|
1034
1530
|
}),
|
|
@@ -1133,6 +1629,17 @@
|
|
|
1133
1629
|
return null
|
|
1134
1630
|
}
|
|
1135
1631
|
|
|
1632
|
+
if (event.artifact?.artifactId?.startsWith("tool-call-")) {
|
|
1633
|
+
const dataPart = (event.artifact?.parts ?? []).find(
|
|
1634
|
+
(p) => p.kind === "data" && p.data?.type === "tool-call",
|
|
1635
|
+
)
|
|
1636
|
+
if (dataPart) {
|
|
1637
|
+
const id = event.artifact.artifactId.slice("tool-call-".length)
|
|
1638
|
+
updateToolCall(id, dataPart.data)
|
|
1639
|
+
}
|
|
1640
|
+
return null
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1136
1643
|
if (event.artifact?.artifactId !== "response") return null
|
|
1137
1644
|
|
|
1138
1645
|
// A2A spec: append/lastChunk are event-level fields (siblings of `artifact`).
|
|
@@ -1151,7 +1658,9 @@
|
|
|
1151
1658
|
}
|
|
1152
1659
|
}
|
|
1153
1660
|
finalizeThinking(streamingBubble, text)
|
|
1661
|
+
finalizeTurnPanel()
|
|
1154
1662
|
thinkingFinalized = true
|
|
1663
|
+
lastStreamingBubble = streamingBubble
|
|
1155
1664
|
streamingBubble = null
|
|
1156
1665
|
streamingText = text // dedupe with handleResult
|
|
1157
1666
|
return null
|
|
@@ -1179,11 +1688,13 @@
|
|
|
1179
1688
|
|
|
1180
1689
|
// Status update with final=true means terminal state
|
|
1181
1690
|
if (event.kind === "status-update" && event.final) {
|
|
1691
|
+
const tokenUsage = event.metadata?.["sap.cds.agents.token-usage"]
|
|
1182
1692
|
return {
|
|
1183
1693
|
id: event.taskId || activeTaskId,
|
|
1184
1694
|
contextId: event.contextId || contextId,
|
|
1185
1695
|
status: event.status,
|
|
1186
1696
|
artifacts: event.artifacts,
|
|
1697
|
+
_tokenUsage: tokenUsage,
|
|
1187
1698
|
}
|
|
1188
1699
|
}
|
|
1189
1700
|
|
|
@@ -1202,11 +1713,8 @@
|
|
|
1202
1713
|
|
|
1203
1714
|
async function cancel() {
|
|
1204
1715
|
const taskId = activeTaskId
|
|
1716
|
+
activeAbort?.abort()
|
|
1205
1717
|
|
|
1206
|
-
// Abort ongoing fetch
|
|
1207
|
-
if (activeAbort) activeAbort.abort()
|
|
1208
|
-
|
|
1209
|
-
// Send tasks/cancel if we have a taskId
|
|
1210
1718
|
if (taskId) {
|
|
1211
1719
|
fetch(AGENT_URL, {
|
|
1212
1720
|
method: "POST",
|
|
@@ -1219,10 +1727,6 @@
|
|
|
1219
1727
|
}),
|
|
1220
1728
|
}).catch(() => {})
|
|
1221
1729
|
}
|
|
1222
|
-
|
|
1223
|
-
addMessage("agent", "⊘ Task canceled.")
|
|
1224
|
-
setBusy(false)
|
|
1225
|
-
prompt.focus()
|
|
1226
1730
|
}
|
|
1227
1731
|
</script>
|
|
1228
1732
|
</body>
|