@brainpilot/web 0.0.5 → 0.0.7
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/assets/index-DWOsU22G.css +1 -0
- package/dist/assets/index-j3rGyO6m.js +445 -0
- package/dist/index.html +2 -2
- package/package.json +6 -3
- package/src/__tests__/agentsReducer.test.ts +67 -0
- package/src/__tests__/api.test.ts +118 -0
- package/src/__tests__/chatScrollBehavior.test.ts +48 -0
- package/src/__tests__/chatScrollMemory.test.ts +49 -0
- package/src/__tests__/demoConversation.test.ts +96 -0
- package/src/__tests__/demoReset.test.ts +24 -0
- package/src/__tests__/internalToolStrip.test.ts +108 -0
- package/src/__tests__/runningToast.test.ts +29 -0
- package/src/__tests__/tokenUsage.test.ts +48 -0
- package/src/__tests__/toolDisplay.test.ts +55 -0
- package/src/__tests__/traceReducer.test.ts +62 -0
- package/src/components/chat/MessageStream.tsx +104 -56
- package/src/components/chat/PromptComposer.tsx +120 -29
- package/src/components/chat/chatScrollMemory.ts +49 -0
- package/src/components/demo/DemoView.tsx +98 -29
- package/src/components/demo/TraceNodeModal.tsx +6 -2
- package/src/components/demo/demoBundle.ts +7 -2
- package/src/components/demo/demoReset.ts +16 -0
- package/src/components/session/AgentNetwork.tsx +68 -75
- package/src/components/session/AgentTraceViews.tsx +35 -70
- package/src/components/session/AnalyticsTab.tsx +58 -224
- package/src/components/session/TraceGraphView.tsx +36 -30
- package/src/components/session/TraceNodeDetail.tsx +61 -24
- package/src/components/session/agentNetworkShared.ts +10 -0
- package/src/components/session/traceLayout.ts +32 -0
- package/src/components/settings/SettingsDialog.tsx +19 -1
- package/src/components/shell/DesktopShell.tsx +72 -17
- package/src/components/sidebar/SessionList.tsx +127 -0
- package/src/components/sidebar/Sidebar.tsx +94 -98
- package/src/contexts/SSEContext.tsx +90 -1
- package/src/contexts/SessionContext.tsx +397 -43
- package/src/contexts/agentsReducer.ts +49 -0
- package/src/contexts/messageGroups.ts +56 -0
- package/src/contexts/messageReducer.ts +4 -0
- package/src/contexts/runningToast.ts +33 -0
- package/src/contexts/traceReducer.ts +62 -0
- package/src/contexts/turnTimer.test.ts +97 -0
- package/src/contexts/turnTimer.ts +108 -0
- package/src/contexts/useTurnTimer.ts +104 -0
- package/src/contracts/backend.ts +53 -2
- package/src/i18n/messages/analytics.ts +16 -6
- package/src/i18n/messages/chat.ts +26 -4
- package/src/i18n/messages/contexts.ts +2 -0
- package/src/i18n/messages/network.ts +13 -9
- package/src/i18n/messages/profile.ts +4 -0
- package/src/i18n/messages/settings.ts +4 -0
- package/src/i18n/messages/shell.ts +2 -0
- package/src/i18n/messages/trace.ts +69 -17
- package/src/mocks/backend.ts +7 -0
- package/src/styles/global.css +289 -70
- package/src/utils/api.ts +105 -8
- package/src/utils/toolDisplay.ts +74 -0
- package/dist/assets/index-C-8G4D4j.js +0 -448
- package/dist/assets/index-C501m5OS.css +0 -1
package/src/styles/global.css
CHANGED
|
@@ -669,6 +669,33 @@ button {
|
|
|
669
669
|
outline: none;
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
/* #104: icon-only nav — square buttons, no horizontal text weight. The label
|
|
673
|
+
lives in a .sr-only span so the accessible name is preserved. */
|
|
674
|
+
.workspace-view-tabs--icon-only button {
|
|
675
|
+
width: 28px;
|
|
676
|
+
justify-content: center;
|
|
677
|
+
gap: 0;
|
|
678
|
+
padding: 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/* #134: a tab that can carry a quiet unread dot (Trace). Positioned so the dot
|
|
682
|
+
sits at the top-right corner of the icon without affecting layout. */
|
|
683
|
+
.workspace-view-tab--badged {
|
|
684
|
+
position: relative;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.workspace-view-tab__badge {
|
|
688
|
+
position: absolute;
|
|
689
|
+
top: 3px;
|
|
690
|
+
right: 3px;
|
|
691
|
+
width: 6px;
|
|
692
|
+
height: 6px;
|
|
693
|
+
border-radius: 999px;
|
|
694
|
+
background: var(--color-danger, #e5484d);
|
|
695
|
+
box-shadow: 0 0 0 2px var(--color-surface-soft);
|
|
696
|
+
pointer-events: none;
|
|
697
|
+
}
|
|
698
|
+
|
|
672
699
|
.session-title {
|
|
673
700
|
display: flex;
|
|
674
701
|
min-width: 0;
|
|
@@ -691,9 +718,10 @@ button {
|
|
|
691
718
|
white-space: nowrap;
|
|
692
719
|
}
|
|
693
720
|
|
|
721
|
+
/* #105: id demoted to muted secondary metadata beside the real title. */
|
|
694
722
|
.session-title__id {
|
|
695
|
-
color: var(--color-text-
|
|
696
|
-
font-size:
|
|
723
|
+
color: var(--color-text-subtle);
|
|
724
|
+
font-size: 11px;
|
|
697
725
|
font-family: var(--font-mono);
|
|
698
726
|
}
|
|
699
727
|
|
|
@@ -858,44 +886,24 @@ button {
|
|
|
858
886
|
border-radius: 0;
|
|
859
887
|
}
|
|
860
888
|
|
|
861
|
-
|
|
889
|
+
/* #79: trace is live-by-SSE — a static "Live" indicator, not a poll toggle. */
|
|
890
|
+
.trace-live-indicator {
|
|
862
891
|
display: inline-flex;
|
|
863
892
|
height: 28px;
|
|
864
893
|
align-items: center;
|
|
865
894
|
gap: 6px;
|
|
866
|
-
|
|
867
|
-
background: transparent;
|
|
868
|
-
color: var(--color-text-muted);
|
|
895
|
+
color: var(--color-text);
|
|
869
896
|
padding: 0 9px;
|
|
870
897
|
font-size: 12px;
|
|
871
898
|
}
|
|
872
899
|
|
|
873
|
-
.trace-live-
|
|
900
|
+
.trace-live-indicator span {
|
|
874
901
|
width: 7px;
|
|
875
902
|
height: 7px;
|
|
876
903
|
border-radius: 999px;
|
|
877
|
-
background: var(--color-text-subtle);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
.trace-live-toggle.is-active {
|
|
881
|
-
color: var(--color-text);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
.trace-live-toggle.is-active span {
|
|
885
904
|
background: var(--color-success);
|
|
886
905
|
}
|
|
887
906
|
|
|
888
|
-
.trace-live-toggle:hover:not(:disabled),
|
|
889
|
-
.trace-live-toggle:focus-visible {
|
|
890
|
-
background: var(--color-hover);
|
|
891
|
-
color: var(--color-text);
|
|
892
|
-
outline: none;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
.trace-live-toggle:disabled {
|
|
896
|
-
opacity: 0.5;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
907
|
.agent-grid {
|
|
900
908
|
display: grid;
|
|
901
909
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
@@ -1168,8 +1176,8 @@ button {
|
|
|
1168
1176
|
|
|
1169
1177
|
.trace-edge path {
|
|
1170
1178
|
fill: none;
|
|
1171
|
-
stroke: var(--color-border-strong);
|
|
1172
|
-
stroke-width:
|
|
1179
|
+
stroke: color-mix(in srgb, var(--color-accent) 55%, var(--color-border-strong));
|
|
1180
|
+
stroke-width: 2;
|
|
1173
1181
|
marker-end: url("#trace-arrow");
|
|
1174
1182
|
}
|
|
1175
1183
|
|
|
@@ -1182,6 +1190,10 @@ button {
|
|
|
1182
1190
|
stroke: var(--color-success);
|
|
1183
1191
|
}
|
|
1184
1192
|
|
|
1193
|
+
.trace-edge--follows path {
|
|
1194
|
+
stroke: color-mix(in srgb, var(--color-accent) 50%, var(--color-border-strong));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1185
1197
|
.trace-edge-label rect {
|
|
1186
1198
|
fill: var(--color-surface-raised);
|
|
1187
1199
|
stroke: var(--color-border-strong);
|
|
@@ -1199,7 +1211,7 @@ button {
|
|
|
1199
1211
|
}
|
|
1200
1212
|
|
|
1201
1213
|
#trace-arrow path {
|
|
1202
|
-
fill: var(--color-border-strong);
|
|
1214
|
+
fill: color-mix(in srgb, var(--color-accent) 55%, var(--color-border-strong));
|
|
1203
1215
|
}
|
|
1204
1216
|
|
|
1205
1217
|
.trace-map-node {
|
|
@@ -1512,6 +1524,13 @@ button {
|
|
|
1512
1524
|
font-size: 11px;
|
|
1513
1525
|
}
|
|
1514
1526
|
|
|
1527
|
+
/* #79: auto-captured (hook-driven) trace nodes get a distinct badge. */
|
|
1528
|
+
.trace-detail__badges span.trace-detail__badge--auto {
|
|
1529
|
+
border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border));
|
|
1530
|
+
background: color-mix(in srgb, var(--color-success) 14%, transparent);
|
|
1531
|
+
color: var(--color-success);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1515
1534
|
.trace-detail__section {
|
|
1516
1535
|
display: grid;
|
|
1517
1536
|
gap: 8px;
|
|
@@ -1622,6 +1641,37 @@ button {
|
|
|
1622
1641
|
white-space: nowrap;
|
|
1623
1642
|
}
|
|
1624
1643
|
|
|
1644
|
+
.trace-detail__children {
|
|
1645
|
+
display: flex;
|
|
1646
|
+
flex-wrap: wrap;
|
|
1647
|
+
gap: 5px;
|
|
1648
|
+
overflow: visible !important;
|
|
1649
|
+
white-space: normal !important;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
.trace-detail__children button {
|
|
1653
|
+
display: inline-flex;
|
|
1654
|
+
min-width: 0;
|
|
1655
|
+
max-width: 100%;
|
|
1656
|
+
height: 24px;
|
|
1657
|
+
align-items: center;
|
|
1658
|
+
border: 1px solid var(--color-border);
|
|
1659
|
+
border-radius: 999px;
|
|
1660
|
+
background: var(--color-surface-soft);
|
|
1661
|
+
color: var(--color-text-muted);
|
|
1662
|
+
padding: 0 8px;
|
|
1663
|
+
font: inherit;
|
|
1664
|
+
font-size: 11px;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.trace-detail__children button:hover,
|
|
1668
|
+
.trace-detail__children button:focus-visible {
|
|
1669
|
+
border-color: var(--color-border-strong);
|
|
1670
|
+
background: var(--color-hover);
|
|
1671
|
+
color: var(--color-text);
|
|
1672
|
+
outline: none;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1625
1675
|
.prompt-home__inner {
|
|
1626
1676
|
display: grid;
|
|
1627
1677
|
width: min(var(--composer-width), 100%);
|
|
@@ -1700,6 +1750,48 @@ button {
|
|
|
1700
1750
|
gap: 4px;
|
|
1701
1751
|
}
|
|
1702
1752
|
|
|
1753
|
+
/* #47: uploaded-file chips shown between the input and the toolbar. */
|
|
1754
|
+
.composer__attachments {
|
|
1755
|
+
display: flex;
|
|
1756
|
+
flex-wrap: wrap;
|
|
1757
|
+
gap: 6px;
|
|
1758
|
+
padding: 0 8px 4px;
|
|
1759
|
+
}
|
|
1760
|
+
.composer__chip {
|
|
1761
|
+
display: inline-flex;
|
|
1762
|
+
align-items: center;
|
|
1763
|
+
gap: 4px;
|
|
1764
|
+
max-width: 220px;
|
|
1765
|
+
padding: 2px 6px 2px 8px;
|
|
1766
|
+
border-radius: 999px;
|
|
1767
|
+
font-size: 12px;
|
|
1768
|
+
line-height: 1.4;
|
|
1769
|
+
background: var(--surface-2, rgba(127, 127, 127, 0.12));
|
|
1770
|
+
color: var(--text-2, inherit);
|
|
1771
|
+
}
|
|
1772
|
+
.composer__chip--pending {
|
|
1773
|
+
opacity: 0.7;
|
|
1774
|
+
}
|
|
1775
|
+
.composer__chip-name {
|
|
1776
|
+
overflow: hidden;
|
|
1777
|
+
text-overflow: ellipsis;
|
|
1778
|
+
white-space: nowrap;
|
|
1779
|
+
}
|
|
1780
|
+
.composer__chip-remove {
|
|
1781
|
+
display: inline-flex;
|
|
1782
|
+
align-items: center;
|
|
1783
|
+
justify-content: center;
|
|
1784
|
+
padding: 0;
|
|
1785
|
+
border: 0;
|
|
1786
|
+
background: transparent;
|
|
1787
|
+
color: inherit;
|
|
1788
|
+
cursor: pointer;
|
|
1789
|
+
opacity: 0.6;
|
|
1790
|
+
}
|
|
1791
|
+
.composer__chip-remove:hover {
|
|
1792
|
+
opacity: 1;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1703
1795
|
.toolbar-select {
|
|
1704
1796
|
padding: 0 8px;
|
|
1705
1797
|
font-size: 13px;
|
|
@@ -1775,20 +1867,11 @@ button {
|
|
|
1775
1867
|
}
|
|
1776
1868
|
|
|
1777
1869
|
@media (max-width: 860px) {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
gap: 16px;
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
.section-heading,
|
|
1787
|
-
.conversation-stack,
|
|
1788
|
-
.nav-item span {
|
|
1789
|
-
display: none;
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1870
|
+
/* #131 — the rail layout at narrow widths is now driven by React
|
|
1871
|
+
(DesktopShell auto-collapses below this breakpoint and applies
|
|
1872
|
+
.desktop-shell--sidebar-collapsed), so the sidebar styling lives in those
|
|
1873
|
+
class rules and the session list is reachable via the rail popover instead
|
|
1874
|
+
of being hidden outright. Only non-sidebar responsive tweaks remain here. */
|
|
1792
1875
|
.prompt-home {
|
|
1793
1876
|
padding-inline: 18px;
|
|
1794
1877
|
}
|
|
@@ -2017,7 +2100,12 @@ button {
|
|
|
2017
2100
|
overscroll-behavior: contain;
|
|
2018
2101
|
padding: 4px 14px 20px 4px;
|
|
2019
2102
|
scrollbar-gutter: stable;
|
|
2020
|
-
scroll-behavior: smooth
|
|
2103
|
+
/* #133: NO `scroll-behavior: smooth` here. The chat stream restores its
|
|
2104
|
+
position imperatively (`scrollTop = …`) on tab-switch remount and on
|
|
2105
|
+
pinned-bottom live append; a global smooth behavior turns those instant
|
|
2106
|
+
jumps into a visible top-to-bottom replay through the history. Smooth
|
|
2107
|
+
scrolling, if ever wanted, must be opt-in around an explicit
|
|
2108
|
+
user-triggered jump, never the default for the container. */
|
|
2021
2109
|
scrollbar-color: var(--color-border) transparent;
|
|
2022
2110
|
scrollbar-width: thin;
|
|
2023
2111
|
}
|
|
@@ -2173,34 +2261,31 @@ button {
|
|
|
2173
2261
|
}
|
|
2174
2262
|
|
|
2175
2263
|
.message-row--live .message-row__body::after {
|
|
2176
|
-
content:
|
|
2177
|
-
position: absolute;
|
|
2178
|
-
inset: 0;
|
|
2179
|
-
pointer-events: none;
|
|
2180
|
-
border-radius: var(--radius-md);
|
|
2181
|
-
background: linear-gradient(
|
|
2182
|
-
100deg,
|
|
2183
|
-
transparent 30%,
|
|
2184
|
-
color-mix(in srgb, var(--color-info) 22%, transparent) 50%,
|
|
2185
|
-
transparent 70%
|
|
2186
|
-
);
|
|
2187
|
-
background-size: 220% 100%;
|
|
2188
|
-
animation: textSweep 1.4s linear infinite;
|
|
2264
|
+
content: none;
|
|
2189
2265
|
}
|
|
2190
2266
|
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
to {
|
|
2196
|
-
background-position: -60% 0;
|
|
2197
|
-
}
|
|
2267
|
+
.message-row__content {
|
|
2268
|
+
display: grid;
|
|
2269
|
+
min-width: 0;
|
|
2270
|
+
gap: 2px;
|
|
2198
2271
|
}
|
|
2199
2272
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2273
|
+
.message-row__content--pending .message-card__content {
|
|
2274
|
+
color: var(--color-text-muted);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
.message-row__streaming-cursor {
|
|
2278
|
+
display: inline-block;
|
|
2279
|
+
width: 7px;
|
|
2280
|
+
height: 15px;
|
|
2281
|
+
border-radius: 2px;
|
|
2282
|
+
background: var(--color-info);
|
|
2283
|
+
opacity: 0.82;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2287
|
+
.message-row__streaming-cursor {
|
|
2288
|
+
animation: pulse 1.1s infinite;
|
|
2204
2289
|
}
|
|
2205
2290
|
}
|
|
2206
2291
|
|
|
@@ -2725,7 +2810,13 @@ button {
|
|
|
2725
2810
|
background: var(--color-surface-soft);
|
|
2726
2811
|
border: 1px solid var(--color-border);
|
|
2727
2812
|
border-radius: 4px;
|
|
2728
|
-
padding
|
|
2813
|
+
/* em-based padding scales with the (0.92em) code font; box-decoration-break
|
|
2814
|
+
keeps the border/radius/background intact on every fragment when an inline
|
|
2815
|
+
code span wraps across lines, instead of slicing into vertically-overflowing
|
|
2816
|
+
boxes that overlap adjacent lines (#130). */
|
|
2817
|
+
padding: 0.1em 0.35em;
|
|
2818
|
+
-webkit-box-decoration-break: clone;
|
|
2819
|
+
box-decoration-break: clone;
|
|
2729
2820
|
}
|
|
2730
2821
|
|
|
2731
2822
|
.message-card__content pre {
|
|
@@ -2865,6 +2956,22 @@ button {
|
|
|
2865
2956
|
font-size: 12px;
|
|
2866
2957
|
}
|
|
2867
2958
|
|
|
2959
|
+
/* #84: label each tool payload as Args / Result so the two stacked <pre>
|
|
2960
|
+
blocks are distinguishable. Label is intentionally low-weight — it's a
|
|
2961
|
+
separator signal, not a heading. */
|
|
2962
|
+
.activity-step__io {
|
|
2963
|
+
display: grid;
|
|
2964
|
+
gap: 4px;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
.activity-step__io-label {
|
|
2968
|
+
color: var(--color-text-muted);
|
|
2969
|
+
font-size: 11px;
|
|
2970
|
+
padding-left: 2px;
|
|
2971
|
+
text-transform: uppercase;
|
|
2972
|
+
letter-spacing: 0.04em;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2868
2975
|
.terminal-input {
|
|
2869
2976
|
display: grid;
|
|
2870
2977
|
grid-template-columns: auto minmax(0, 1fr);
|
|
@@ -2920,6 +3027,62 @@ button {
|
|
|
2920
3027
|
opacity: 0;
|
|
2921
3028
|
}
|
|
2922
3029
|
|
|
3030
|
+
/* #131 — collapsed icon-rail session popover. The rail hides the inline session
|
|
3031
|
+
section (rule above) and squeezes nav labels to opacity:0; the popover is a
|
|
3032
|
+
floating panel anchored to the Sessions icon that restores the full list. */
|
|
3033
|
+
.sidebar__sessions-popover-anchor {
|
|
3034
|
+
position: relative;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
.sidebar__sessions-popover {
|
|
3038
|
+
position: absolute;
|
|
3039
|
+
top: 0;
|
|
3040
|
+
left: calc(100% + 10px);
|
|
3041
|
+
z-index: 40;
|
|
3042
|
+
display: flex;
|
|
3043
|
+
width: 300px;
|
|
3044
|
+
max-height: min(70vh, 560px);
|
|
3045
|
+
flex-direction: column;
|
|
3046
|
+
gap: 8px;
|
|
3047
|
+
padding: 12px;
|
|
3048
|
+
border: 1px solid var(--color-border);
|
|
3049
|
+
border-radius: var(--radius-md);
|
|
3050
|
+
background: var(--color-surface-raised, var(--color-surface));
|
|
3051
|
+
box-shadow: var(--shadow-md, 0 12px 32px rgba(0, 0, 0, 0.18));
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
.sidebar__sessions-popover .conversation-stack {
|
|
3055
|
+
display: grid;
|
|
3056
|
+
min-height: 0;
|
|
3057
|
+
overflow-y: auto;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
.sidebar__sessions-popover-head {
|
|
3061
|
+
display: flex;
|
|
3062
|
+
align-items: center;
|
|
3063
|
+
justify-content: space-between;
|
|
3064
|
+
gap: 8px;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
.sidebar__sessions-popover-head h2 {
|
|
3068
|
+
margin: 0;
|
|
3069
|
+
color: var(--color-text-subtle);
|
|
3070
|
+
font-size: 13px;
|
|
3071
|
+
font-weight: 500;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/* The popover lives inside the collapsed rail, so undo the rail's label
|
|
3075
|
+
squeeze/hide for everything inside it — its content is fully expanded. */
|
|
3076
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .nav-item span,
|
|
3077
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .conversation-row span,
|
|
3078
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .conversation-row small {
|
|
3079
|
+
opacity: 1;
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
.desktop-shell--sidebar-collapsed .sidebar__sessions-popover .conversation-stack {
|
|
3083
|
+
display: grid;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
2923
3086
|
.sandbox-status {
|
|
2924
3087
|
position: relative;
|
|
2925
3088
|
display: inline-flex;
|
|
@@ -5097,6 +5260,52 @@ button {
|
|
|
5097
5260
|
font-size: 11px;
|
|
5098
5261
|
}
|
|
5099
5262
|
|
|
5263
|
+
.agent-network__available {
|
|
5264
|
+
border: 1px solid var(--color-border);
|
|
5265
|
+
border-radius: var(--radius-sm);
|
|
5266
|
+
background: var(--color-surface);
|
|
5267
|
+
color: var(--color-text-muted);
|
|
5268
|
+
padding: 7px 10px;
|
|
5269
|
+
font-size: 11px;
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
.agent-network__available summary {
|
|
5273
|
+
cursor: pointer;
|
|
5274
|
+
list-style: none;
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5277
|
+
.agent-network__available summary::-webkit-details-marker {
|
|
5278
|
+
display: none;
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
.agent-network__available summary::before {
|
|
5282
|
+
content: ">";
|
|
5283
|
+
display: inline-block;
|
|
5284
|
+
margin-right: 6px;
|
|
5285
|
+
color: var(--color-text-subtle);
|
|
5286
|
+
transition: transform var(--ease-standard);
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
.agent-network__available[open] summary::before {
|
|
5290
|
+
transform: rotate(90deg);
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
.agent-network__available ul {
|
|
5294
|
+
display: flex;
|
|
5295
|
+
flex-wrap: wrap;
|
|
5296
|
+
gap: 5px;
|
|
5297
|
+
margin: 8px 0 0;
|
|
5298
|
+
padding: 0;
|
|
5299
|
+
list-style: none;
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5302
|
+
.agent-network__available li {
|
|
5303
|
+
border: 1px dashed var(--color-border-strong);
|
|
5304
|
+
border-radius: 999px;
|
|
5305
|
+
color: var(--color-text-subtle);
|
|
5306
|
+
padding: 2px 7px;
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5100
5309
|
.agent-network__legend-dot {
|
|
5101
5310
|
display: inline-block;
|
|
5102
5311
|
width: 8px;
|
|
@@ -6395,6 +6604,17 @@ button {
|
|
|
6395
6604
|
.agent-analytics__chart-title svg {
|
|
6396
6605
|
color: var(--color-text-subtle);
|
|
6397
6606
|
}
|
|
6607
|
+
.agent-analytics__token-total {
|
|
6608
|
+
font-size: 22px;
|
|
6609
|
+
font-weight: 700;
|
|
6610
|
+
color: var(--color-text);
|
|
6611
|
+
margin: 2px 0 8px;
|
|
6612
|
+
}
|
|
6613
|
+
.agent-analytics__token-total-label {
|
|
6614
|
+
font-size: 12px;
|
|
6615
|
+
font-weight: 500;
|
|
6616
|
+
color: var(--color-text-muted);
|
|
6617
|
+
}
|
|
6398
6618
|
.agent-analytics__svg {
|
|
6399
6619
|
width: 100%;
|
|
6400
6620
|
height: auto;
|
|
@@ -7426,4 +7646,3 @@ button {
|
|
|
7426
7646
|
.demo-right { border-bottom: 1px solid var(--color-border); }
|
|
7427
7647
|
.demo-landing__cards { grid-template-columns: 1fr; }
|
|
7428
7648
|
}
|
|
7429
|
-
|
package/src/utils/api.ts
CHANGED
|
@@ -39,10 +39,29 @@ const API_BASE = "/api";
|
|
|
39
39
|
// Trust-front: the hosted gateway authenticates via an httpOnly cookie that the
|
|
40
40
|
// browser carries automatically. The frontend never reads, stores, or attaches a
|
|
41
41
|
// token — it just makes credentialed requests.
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
//
|
|
43
|
+
// #106: callers that drive composer state (postMessage / create) pass a
|
|
44
|
+
// `timeoutMs`. A hung request used to leave `isSending` true forever (the
|
|
45
|
+
// `finally` that resets it never ran), permanently disabling the composer and
|
|
46
|
+
// silently dropping the user's input. With a timeout the request rejects, the
|
|
47
|
+
// caller's catch surfaces a recoverable error, and `isSending` is released.
|
|
48
|
+
function apiFetch(
|
|
49
|
+
input: RequestInfo | URL,
|
|
50
|
+
init: RequestInit & { timeoutMs?: number } = {},
|
|
51
|
+
): Promise<Response> {
|
|
52
|
+
const { timeoutMs, signal, ...rest } = init;
|
|
53
|
+
if (timeoutMs == null) {
|
|
54
|
+
return fetch(input, { credentials: "include", signal, ...rest });
|
|
55
|
+
}
|
|
56
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
57
|
+
// Honour an upstream signal too, if one was supplied.
|
|
58
|
+
const merged = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
59
|
+
return fetch(input, { credentials: "include", signal: merged, ...rest });
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
/** #106: default ceiling for composer-driving requests (create / postMessage). */
|
|
63
|
+
const SEND_TIMEOUT_MS = 30_000;
|
|
64
|
+
|
|
46
65
|
function authHeaders(json = true): Record<string, string> {
|
|
47
66
|
return json ? { "Content-Type": "application/json" } : {};
|
|
48
67
|
}
|
|
@@ -69,6 +88,21 @@ async function handleJson<T>(res: Response): Promise<T> {
|
|
|
69
88
|
return (await res.json()) as T;
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
/** #47: encode a Blob/File as base64 (without the data: prefix) for upload. */
|
|
92
|
+
function blobToBase64(blob: Blob): Promise<string> {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const reader = new FileReader();
|
|
95
|
+
reader.onerror = () => reject(reader.error ?? new Error("file read failed"));
|
|
96
|
+
reader.onload = () => {
|
|
97
|
+
const result = reader.result as string;
|
|
98
|
+
// strip the "data:<mime>;base64," prefix
|
|
99
|
+
const comma = result.indexOf(",");
|
|
100
|
+
resolve(comma >= 0 ? result.slice(comma + 1) : result);
|
|
101
|
+
};
|
|
102
|
+
reader.readAsDataURL(blob);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
72
106
|
export function getSSEUrl(sessionId: string): string {
|
|
73
107
|
// Same origin; relative path lets EventSource follow the current host/port and
|
|
74
108
|
// carry the auth cookie automatically — no token in the query string.
|
|
@@ -246,6 +280,20 @@ export const api = {
|
|
|
246
280
|
throw new Error(await parseError(res));
|
|
247
281
|
}
|
|
248
282
|
},
|
|
283
|
+
|
|
284
|
+
// #47: upload a file into the workspace (base64 over the JSON byte chain).
|
|
285
|
+
async uploadFile(sandboxId: string, path: string, file: Blob): Promise<{ path: string; size: number }> {
|
|
286
|
+
const contentBase64 = await blobToBase64(file);
|
|
287
|
+
const res = await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files`, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { ...authHeaders(), "content-type": "application/json" },
|
|
290
|
+
body: JSON.stringify({ path, contentBase64 }),
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
throw new Error(await parseError(res));
|
|
294
|
+
}
|
|
295
|
+
return handleJson(res);
|
|
296
|
+
},
|
|
249
297
|
},
|
|
250
298
|
|
|
251
299
|
sessions: {
|
|
@@ -287,6 +335,7 @@ export const api = {
|
|
|
287
335
|
await apiFetch(`${API_BASE}/sessions`, {
|
|
288
336
|
method: "POST",
|
|
289
337
|
headers: authHeaders(),
|
|
338
|
+
timeoutMs: SEND_TIMEOUT_MS,
|
|
290
339
|
body: JSON.stringify({
|
|
291
340
|
title,
|
|
292
341
|
...(opts.providerId ? { providerId: opts.providerId } : {}),
|
|
@@ -294,7 +343,16 @@ export const api = {
|
|
|
294
343
|
}),
|
|
295
344
|
}),
|
|
296
345
|
);
|
|
297
|
-
|
|
346
|
+
// The runtime's POST /sessions returns the envelope `{ id, session }`
|
|
347
|
+
// (server.ts), unlike GET /sessions[/:id] which return the bare session.
|
|
348
|
+
// Unwrap `session` if present so normalizeSession reads the real `title`
|
|
349
|
+
// instead of falling back to `Session <id8>` (#96). Tolerate a bare
|
|
350
|
+
// object too (mock / future shape change).
|
|
351
|
+
const envelope = raw as { session?: unknown } | null;
|
|
352
|
+
const sessionRaw = envelope && typeof envelope === "object" && "session" in envelope
|
|
353
|
+
? envelope.session
|
|
354
|
+
: raw;
|
|
355
|
+
return normalizeSession(sessionRaw as Parameters<typeof normalizeSession>[0]);
|
|
298
356
|
},
|
|
299
357
|
|
|
300
358
|
async update(sessionId: string, title: string): Promise<Session> {
|
|
@@ -323,15 +381,19 @@ export const api = {
|
|
|
323
381
|
);
|
|
324
382
|
},
|
|
325
383
|
|
|
326
|
-
async interrupt(sessionId: string): Promise<{
|
|
384
|
+
async interrupt(sessionId: string): Promise<{ interrupted: boolean }> {
|
|
327
385
|
if (runtimeConfig.useMockBackend) {
|
|
328
|
-
return {
|
|
386
|
+
return { interrupted: true };
|
|
329
387
|
}
|
|
330
|
-
|
|
331
|
-
|
|
388
|
+
// #90: Stop = whole-session interrupt. Hit the dedicated interrupt route
|
|
389
|
+
// (RUNTIME_ROUTES.interrupt), NOT /messages — the messages endpoint's body
|
|
390
|
+
// schema rejects {type:"interrupt"} so the agent was never actually
|
|
391
|
+
// stopped. Empty body = interrupt every agent in the session.
|
|
392
|
+
return handleJson<{ interrupted: boolean }>(
|
|
393
|
+
await apiFetch(`${API_BASE}/sessions/${sessionId}/interrupt`, {
|
|
332
394
|
method: "POST",
|
|
333
395
|
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
334
|
-
body: JSON.stringify({
|
|
396
|
+
body: JSON.stringify({}),
|
|
335
397
|
}),
|
|
336
398
|
);
|
|
337
399
|
},
|
|
@@ -348,6 +410,7 @@ export const api = {
|
|
|
348
410
|
await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
|
|
349
411
|
method: "POST",
|
|
350
412
|
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
413
|
+
timeoutMs: SEND_TIMEOUT_MS,
|
|
351
414
|
body: JSON.stringify({
|
|
352
415
|
type: payload.type ?? "user_message",
|
|
353
416
|
content: payload.content,
|
|
@@ -419,6 +482,40 @@ export const api = {
|
|
|
419
482
|
return Array.isArray(raw.events) ? (raw.events as RawAgUiEvent[]) : [];
|
|
420
483
|
},
|
|
421
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Persisted AG-UI event history from `events.jsonl` — used to rehydrate
|
|
487
|
+
* the chat list (and trace/agents seed) when a session is activated after
|
|
488
|
+
* a runtime restart. SSE only replays the in-memory ring buffer; this
|
|
489
|
+
* endpoint walks the on-disk log and returns the tail when long. Pass
|
|
490
|
+
* `limit: 0` to request the full log for lossless rehydrate.
|
|
491
|
+
*
|
|
492
|
+
* Tolerates any non-200 / non-JSON response by returning an empty
|
|
493
|
+
* envelope, so callers can fall through to whatever live data the SSE
|
|
494
|
+
* stream eventually delivers.
|
|
495
|
+
*/
|
|
496
|
+
async getHistory(
|
|
497
|
+
sessionId: string,
|
|
498
|
+
opts: { limit?: number } = {},
|
|
499
|
+
): Promise<{ events: RawAgUiEvent[]; total: number; truncated: boolean }> {
|
|
500
|
+
if (runtimeConfig.useMockBackend) {
|
|
501
|
+
return { events: [], total: 0, truncated: false };
|
|
502
|
+
}
|
|
503
|
+
const qs = opts.limit !== undefined ? `?limit=${encodeURIComponent(opts.limit)}` : "";
|
|
504
|
+
const res = await apiFetch(
|
|
505
|
+
`${API_BASE}/sessions/${sessionId}/history${qs}`,
|
|
506
|
+
{ headers: authHeaders() },
|
|
507
|
+
);
|
|
508
|
+
if (!res.ok) return { events: [], total: 0, truncated: false };
|
|
509
|
+
const raw = (await res.json().catch(() => null)) as
|
|
510
|
+
| { events?: unknown[]; total?: number; truncated?: boolean }
|
|
511
|
+
| null;
|
|
512
|
+
return {
|
|
513
|
+
events: Array.isArray(raw?.events) ? (raw!.events as RawAgUiEvent[]) : [],
|
|
514
|
+
total: typeof raw?.total === "number" ? raw!.total : 0,
|
|
515
|
+
truncated: Boolean(raw?.truncated),
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
|
|
422
519
|
async state(sessionId: string): Promise<SessionStateSnapshot> {
|
|
423
520
|
if (runtimeConfig.useMockBackend) {
|
|
424
521
|
return mockBackend.state();
|